Updating parent's object data from child controller - javascript

I have a object $scope.object in parentCtrl, and I have a isolated scope directive. Inside directive, I have a functionality wherein I need to make an API call and fetch one of the property of $scope.object (this property is an array of object) and replace this property in original $scope.object.
I have tried various ways of doing it but somehow I am not exactly getting at it. I have tried updating it using $scope.$parent.object from directive's controller.
After this I tried sending the fetched array in to the parent's controller and replacing it there.
Once the $scope.object gets updated, the directive should run as the directive's template is binded with properties of $scope.object.
The interesting thing is that the view gets updated with new data but the data in original object somehow again becomes null.
I guess, there is some issue with the way my directive is called as the same directive is called nestedly. As shown below:
<li ng-repeat="t in A" ng-if="t.selected">
<div directive> <div>
</li>
where, A is the fetched property and t.selected is true. Now in the fetched data t.selected is true for two elements of array, while the directive is called 4 times, twice for each element. In the first iteration the data remains intact in parent object, while in 2nd iteration, data becomes null
I want to know, how do I update the data of parent object permanently
Directive:
<li ng-repeat="(key, t) in fetch.data>
<div layout="row" layout-wrap directive target="t" ></div>
</li>
and this target is double way binded in directive's scope with parent's scope

Related

document getElementById returns null in ng-repeated dom elements

I have ng-repeat loop with canvas items:
<ul>
<li ng-repeat="item in todos">
<canvas id="canvas-{{item.Guid}}"></canvas>
</li>
</ul>
Somewhere in code, there is function addTodo, which is somehow like:
$scope.todos.push(newTodo);
$scope.renderPDF(newTodo.Attachment,cxId);
Where cxId is canvas id and renderPDF renders something on canvas using getElementById.
But when I add todo, although in Chrome Developer Console there is canvas with correct id, I get error that getElementById on cxId returns null.
All is done correctly and Angular uses two-way data binding, so why there is a problem?
I wanted to explain, since it's peculiar.
Angular uses Two way data-binding, but in ng-book i read about this process.
And all parameters that are in view ( in this case, also todos which generate canvas-id ), all these parameters which are in scope are set watch on them. On their change, there goes change to a view. This process is two-way data binding. It happens in digest cycle. So there is a time to do this and when i am pushing todo to array it's not started this digest cycle yet.
Solution is to enforce digest cycle after adding element to array, what will listen to changes in scope variables:
Instead of:
$scope.todos.push(newTodo);
$scope.renderPDF(newTodo.Attachment,cxId);
There should be:
$scope.todos.push(newTodo);
$scope.apply();
$scope.renderPDF(newTodo.Attachment,cxId);
apply method enforces digest cycle.

Using ng-repeat with directive causes flaw in watch

I have made a new directive that is basically a new carousel that extends the angular UI carousel bootstrap. This new carousel will display multiple divs in one frame. My new directive accepts any data in an array and a custom html template for each data.
However, if I use my carousel with a directive, there is a weird behaviour that I am seeing with watch inside the directive. Everything works fine, but the watch inside my directive is always getting the same value for newVal and oldVal. What I mean is this, here's my carousel code:
<slide ng-repeat="frame in ctrl.frames">
<div ng-repeat="divs in frame.divs">
<custom-directive data="divs"></custom-directive>
</div>
</slide>
and inside my customDirective controller, I watch the change of data like this:
$scope.$watch("ctrl.data", function(newVal, oldVal){
if (newVal !== oldVal) {
// data is updated, redraw the directive in my case
// however newVal is always the same as oldVal
}
})
newVal and oldVal is always the same.. I expected the initial state to be oldVal = undefined and newVal will be my new data. However, this is never the case. Data is passed as a two-way binding to carousel and to custom directive (using '=' operator inside the scope of each directive).
Why is this happening? I have investigated this for long and here's my findings:
If I don't use ng-repeat inside my carousel, this will work. oldVal will be undefined and newVal will be my data during the initial state. But why is ng-repeat causing this? I have read lots of article regarding golden rule of prototypical inheritance, that says ng-repeat will create new childScope that hides/shadows the parent, but that only happens to primitive object and I am passing an array to my data.
I need to use ng-repeat in my carousel directive.. so I need to know why ng-repeat is causing this.. any suggestions?
UPDATE:
Reproduced the problem in Plunkr here. As you can see, oldValue is always the same as newValue (I expected the oldValue to be undefined in the beginning)
When you register the $watch in your link function, Angular has already processed the bindings during the preLink phase, hence you will never see undefined the first time your watcher is executed (that initialiation call is the only moment on which oldVal and newVal are potentially the same. If the watcher was registered before the bindings resolution, the oldValue would be undefined)
If you really want to see it, you could override the compile phase and add a custom preLink method (the default link being the postLink).
But I really doubt that you want to do that. Why is it a problem to not have undefined the first time? You should try to explain the real problem you are facing.
Also, note that if divs that you pass to your directive is an array, you should use scope.$watchColleciton instead of scope.$watch in order to detect changes in the array elements instead of change of the whole array pointer.
I think the problem you are having is just a misunderstanding of how $watch works.
$watch is expected to initialize with equal values. See the documentation here. Specifically:
After a watcher is registered with the scope, the listener fn is
called asynchronously (via $evalAsync) to initialize the watcher. In
rare cases, this is undesirable because the listener is called when
the result of watchExpression didn't change. To detect this scenario
within the listener fn, you can compare the newVal and oldVal. If
these two values are identical (===) then the listener was called due
to initialization
So in other words, your check for if they are equal is so you don't detect the initial call
In your provided Plunker, if you need to do some initialization code, you can do two things:
You can check if they are equal in the $watch function, and if they are then that is the initial call with their initial values
Or, outside of that function in the link function, the values are their initial values there (since the link function is equivalent to post-link, which means scope values have already been linked) so you can put your code there
Forked your Plunker here. Notice I moved the alert outside of the $watch, and the value is still valid
EDIT:
The reason you see a difference when it is not in the ng-repeat and it is set up like your commented out code in the Plunkr is due to you adding data in a $timeout. When the page initially loads, below are what the two types render as:
<a1 prop="data[0]"></a1>
HTML looks as it was written. data=[]. directive element exists, calling link with data[0]=undefined. $watch called with prop=undefined
<!-- ngRepeat: element in data track by $index -->
HTML is simply a comment. waiting for data to be populated. No directive element exists, which means link is not called
When you add items to data after the timeout, they look like this:
<a1 prop="data[0]"></a1>
Same as above. data[0] is now defined so prop is defined
<div ng-repeat="element in data track by $index" class="ng-scope">
<a1 prop="element" class="ng-isolate-scope"></a1>
</div> (x3)
Page now has directive elements. calling link function on each with data now filled. $watch called with prop values linked

Angular ngModel binding on first level method of $scope

This is not exactly a question with some code, but more for my understandings. So please forgive me if this isnt appropriate here..
I was playing with a simple checkbox with an ngModel assigned:
<input type="checkbox" ng-model="someFlag"/>
I expect this to bind the boolean of the checkbox to $scope.someFlag (if controller and everything else configured properly).
And yes, it does work. But there are times where I found that this is not working. Case: When I try to do something when the someFlag changes (for example in a $watch) the value is not really binded.
But then I came accross something a collegua at work mentioned once:
Use a wrapper object
Doing it like that works now without any problems:
<input type="checkbox" ng-model="wrapperObject.someFlag"/>
Watching $scope.wrapperObject.someFlag works as expected.
Now the question: Why??
When ngModel directive is executed, it reads attribute's ng-model value (in this case, "someFlag") and saves it in it's local function scope (not to confuse to angulars $scope). But since a boolean in javascript is a primitive, you cannot pass it by reference, only by value. That means only the $scope.someFlag's value (true or false) get's copied to ngModel, and not the way of accessing and modifying $scope.someFlag from ngModel.
When you use wrapper object, it is passed by reference, meaning, it is the same thing both in $scope.wrapperObject and in ngModel's local function scope, because it points to the same memory address behind the scenes.
I'm not sure if this explanatory enough, but when working with primitive values in angular, you must keep in mind the difference between passing by reference and by value and that when primitive's change, other parts of application might not know this.
In my understanding you want to pass Boolean value if check box checked to controller
try this:
In your html:
<input type="checkbox" ng-model="value"
ng-true-value="YES" ng-false-value="NO"> <br/>
in your controller:
$scope.value is gives Boolean value//
This is because scope inheritance. A nice article about angularjs scope and inheritance
I guess the $watch logic you mentioned might have been triggered from inside a ng-repeat or a ng-if. Remember angularjs creates a new scope variable for each object inside ng-repeat and ng-if.
<input type="checkbox" ng-model="someFlag"/> //This is a primitive $scope.someFlag
<input type="checkbox" ng-model="obj.someFlag"/> //This is an object $scope.obj.someFlag
<div ng-repeat="opt in options">
Here a new child scope - $scope.somechild is created for the opt variable.
So if primitive variable someFlag is referenced here it will be taken as $scope.child.someFlag. so any update here will not update the parent $scope.someFlag.
But if an object obj.someFlag is referenced here by object inheritance the compiler will try to find obj.someFlag in child - $scope.child as it is not present it will search in parent $scope as it is present this will now refer to $scope.obj.someFlag and any modification done here is done in the actual parent scope.
This is part of the mantra: "there should always be a dot in your model" by Misko Hevery, one of the fathers of Angular. You can, and probably should, watch this video: https://www.youtube.com/watch?v=ZhfUv0spHCY&feature=youtu.be&t=32m51s
ngModel is a directive that does two-way-data-binding. Binding to a primitive (Boolean in this case) will make the setter to set it on the current scope, rather than on the scope that's defined which could interfere with other scopes.
In your scenario, in a parent scope we have your $scope.someFlag = true. In a child scope we then have your input:
<input type="checkbox" ng-model="someFlag"/>
That will work initially but once the user changes the value, the someFlag will be created on the child scope and the bindings will read and write from that value from now on.
I hope this is, somehow, clear.
Note that this only happens with two-way-data-binding directives and not regular ones such as ngDisabled or ngHide

Pass parent scope value into ng-repeat loop in Angular

This should be an extremely simple question, but all of the workarounds I've found are complex. I'm looping through an array of objects in using ng-repeat in a template as follows:
<div class="row-fluid" ng-repeat="message in messages.current|filter:'draft'">
{{ message.subject }} ... {{ campaign.name }} ...
</div>
Since the ng-repeat creates a new scope, the 'campaign' object from the controller doesn't seem to be accessable. Is there any way (aside from adding the campaign object to every item in my array) of getting that value?
Thanks in advance.
You can access the parent scope by using $parent
<div class="row-fluid" ng-repeat="message in messages.current|filter:'draft'">
{{ message.subject }} ... {{ $parent.campaign.name }} ...
</div>
This is a way that works that doesn't use $parent. It searches upwards through the nested scopes to find the object you're using, however many scopes it has to go through.
In the scope that contains the list, you can define an object with the list as a property, like this:
$scope.obj = {};
$scope.obj.items = ['item1','item2','item3'];
Then have the ng-repeat look like this:
<div ng-repeat="item in obj.items | filter:'item3' track by $index">
{{obj.items[ obj.items.indexOf(item) ]}}
</div>
(you need to use obj.items[ obj.items.indexOf(item) ] rather than obj.items[ $index ] because $index is the index of the filtered array, not the original)
The reason this works is because while obj doesn't exist in the current scope, as you attempt to access its property, Angular will look above the current scope rather than give you an error (if you just tried {{obj}} it would be undefined, and Angular would be happy with giving you nothing instead of looking through higher scopes). This is a helpful link about nested scopes: http://www.angularjshub.com/examples/basics/nestedcontrollers/
In my case I needed the track by $index, because I had an input with ng-model bound to an item in the array, and whenever the model updated, the input would blur because I think the HTML was being re-rendered. A consequence of using track by $index is that items in the array with identical values will be repeated. If you modify one of those other than the 1st one, weird things will happen. Maybe you can filter for uniqueness to avoid that.
I'm relatively new to AngularJS, so please comment if there is anything big I'm missing. But this works, so I'm using it at least.
Another method might be to pass parent scope as a scope variable to the directive i.e.
<my-directive
md-parent-scope="this"
ng-repeat="item in items"></my-directive>
It's a bit messy, but you have more control over what the parent actually is and can pass anything in.

In AngularJS, how to make an isolated scope inherit from ng-repeat's scope

I'm trying to create a custom component that receives arguments in a ng-repeat loop.
So for example, say I have a component named "mycomp" that receives a custom argument "name" in a ng-repeat:
<mycomp name="{obj.name}" ng-repeat="obj in list" />
And in my directive the isolated scope is defined like this:
scope:{name:"#"}
That won't work because ng-repeat creates an isolated scope for each element it iterates. So I ended up having two levels of scopes.
How do I get around this issue? Am I doing something wrong?
Thanks.
As I stated in my comment of your original question, this has already been answered. Anyway, here it is, summed up:
In your template, state the model you want to have inherited, without {{}} (as using brackets results in the value being passed, and not the reference to the model itself):
<mycomp name="obj.name" ng-repeat="obj in list" />
And in your directive, establish a 2-way binding, like so:
scope:{name:"="}
EDIT:
I realize now (after your comment) that while this solves your problem, it doesn't fully answer the question. Here goes:
When you create a directive you have the choice of creating a scope that inherits from its parent (controller, typically, though not necessarily) ou an "isolated" scope, by specifying scope: true or scope: {...}, respectively.
So, by creating an unisolated scope, all the parent's models are available (you can access scope.obj - created via ng-repeat - but also scope.list). This is convenient, but also dangerous, of course (and doesn't really create reusable code).
If you create an isolated scope, you can specify the scope's models using '#', '=' or '&'.
'#' and '&' both produce a isolated, unbinded value, (that, if you change, changes only on the isolated scope - in your case, the object in the original list suffers no change at all), the only difference being that '#' reads a string value, and '&' reads an expression.
THIS IS IMPORTANT: the reason why I believe your code didn't work was (only) because you passed name="{obj.name}" and not name="{{obj.name}}", for with '#' the string value is read, and that string value can be the name of obj, but you must include it in {{}}!
If you use '=', you are declaring that you want that variable to be binded with the specified outside variable. So, if (in a fit of crazy, crazy rage!) you want to have 2 models in your directive that start up with the same value, but on is binded (i.e. changes are propagated to the outside scope), you could do something like this:
<mycomp binded-name="obj.name" unbinded-name="{{obj.name}}" ng-repeat="obj in list" />
and in your directive:
scope:{
bindedName: "=",
unbindedName: "#"
}

Categories