AngularJS: Setting a variable in a ng-repeat generated scope - angularjs

How do I access the set of scopes generated by an ng-repeat?
I suppose at the heart of this is the fact that I don't understand quite how the relationship works between a) the collection of objects that I pass into the ng-repeat directive and b) the collection of scopes that it generates. I can play around with (a), which the ng-repeat scope watches and picks up, but how do I set variables on the scope itself (b)?
My use case is that I have a set of elements repeating using ng-repeat, each of which has an edit view that gets toggled using ng-show/ng-hide; the state for each element is held in a variable in the local scope. I want to be able to trigger an ng-show on a particular element, but I want the trigger to be called from outside the ng-repeat, so I need to be able to access the local scope variable.
Can anyone point me in the right direction (or tell me if I'm barking up the wrong tree)?
Thanks
Update: Link below was very helpful thankful. In the end I created a directive for each of the repeating elements, and used the directive's link function to add its scope to a collection on the root scope.

Here is a pretty simple way to do what I think you are trying to do (I figured this out when I needed to do something similar):
We know that each repeated item has it's own scope created for it. If we could pass this scope to a method defined on the parent scope, then we'd be able to do what we want with it in terms of manipulating or adding properties. It turn out this can be done by passing this as an argument:
Example
// collection on controller scope
$scope.myCollection = [
{ name: 'John', age: 25 },
{ name: 'Barry', age: 43 },
{ name: 'Kim', age: 26 },
{ name: 'Susan', age: 51 },
{ name: 'Fritz', age: 19 }
];
// template view
<ul>
<li ng-repeat="person in myCollection">
<span ng-class="{ bold : isBold }">{{ person.name }} is aged {{ person.age }} </span>
<button class="btn btn-default btn-xs" ng-click="toggleBold(this)">toggle bold</button>
</li>
</ul>
So when we press the "toggle bold" button, we are calling the $scope.toggleBold() method that we need to define on the controller's $scope. Notice that we pass this as the argument, which is actually the current ng-repeat scope object.
Therefore we can manipulate it like this
$scope.toggleBold = function(repeatScope) {
if (repeatScope.isBold) {
repeatScope.isBold = false;
} else {
repeatScope.isBold = true;
}
};
Here is a working example: http://plnkr.co/edit/Vg9ipoEP6wxG8M1kpiW3?p=preview

Ben Nadel has given a pretty clean solution to the "how do I assign to an ngRepeat's $scope" problem, which I just implemented in my own project. Essentially, you can add an ngController directive alongside your ngRepeat, and manipulate ngRepeat's $scope inside the controller.
Below is my own contrived example, which demonstrates assigning to ngRepeat's $scope in a controller. Yes, there are better ways to do this exact thing. See Ben Nadel's post for a better example.
<div ng-controller="ListCtrl">
<h1>ngRepeat + ngController</h1>
<ul>
<li ng-repeat="item in items" ng-controller="ItemCtrl" ng-show="isVisible">
{{item.name}}
<button ng-click="hide()">hide me!</button>
</li>
</ul>
</div>
<script type="text/javascript">
var app = angular.module("myApp", []);
app.controller("ListCtrl", function($scope) {
$scope.items = [
{name: "Item 1"},
{name: "Item 2"},
{name: "Item 3"}
];
});
app.controller("ItemCtrl", function($scope) {
$scope.isVisible = true;
$scope.hide = function() {
$scope.isVisible = false;
};
});
</script>
EDIT: Having re-read your question, seeing that you need to manipulate a bunch of child scopes in a parent scope, I think that your directive(s) approach is the way to go. I still think this answer may be useful to some, as I came across your question while looking for this answer.

When working within a hierarchy of scopes I find very useful to dispatch events with $emit and $broadcast.
$emit dispatches an event upwards so your child scopes can notify parent scopes of a particular event.
$broadcast is the other way round.
Alternatively, as child scopes have access to parent scope properties you could trigger changes by using $watch on a particular property in the parent scope.
UPDATE: As for accessing the child scopes, this may turn useful for you : Get to get all child scopes in Angularjs given the parent scope

Related

how to create personal scope for each compiled element?

When I create new 'wItem' object and want to append compiled DOM to container(not ng-repeat)
container.append($compile(UIService.appendItem())($scope));
var children = container[0].children;
var length = children.length-1;
//take appended element and set new scope Item there
var newEl = children[length];
angular.element(newEl).scope().item = wItem;
UIService.appendItem
function appendWorkItem() {
return '<div layout="column" class=" workItem workItemName" id={{item._id}}child > ' +
'<div>{{item.name}}</div> ' +
' </div>'
}
the result is:
First time
wItem[0] = {_id:1, name: item1}
, it creates wItem and appends to DOM!
second and other times I should be:
wItem[1] = {_id:2, name: item2}
wItem[2] = {_id:3, name: item3}
it creates new wItem and when it appends to DOM it updates previous created elements with the last wItem scope.and I getting this:
I am not so good in it,but I think it passes scope of last element to prev ones. How to fix it?
First, don't manipulate the DOM from your controller. Controllers provide scope and are generally nothing but scoping glue for views and models.
Get familiar with directives because they're practically required. Similarly it is critical to understand scopes. Failing to understand scopes and directives will doom your angular efforts.
Here's a directive implementing your UIService. It can be done other ways.
.directive("workItem", ['UIService', function(uiservice) {
return {
scope: {
item: "="
},
template: function() {
return uiservice.appendWorkItem();
}
}
}])
As has been pointed out you need to familiarize yourself with the ng-repeat directive. An example using the workItem directive and ng-repeat might look like this
<div ng-controller="WorkItems">
<div ng-repeat="item in items">
<work-item item="item"></work-item>
</div>
</div>
Here's a plnkr demonstrating the above directive used with ng-repeat.
I suggest staying away from $compile until you have a good grasp of directives and scopes (and actually have a bona fide reason to use it).

What sense does have a new child scope inaccessible from parent controller? (created by ng- directives)

In angular.js Some directives create child scopes. (ng-include, ng-if, etc)
I know there are ways to solve it, for example by declaring the variable in the scope of the controller. Just uncomment //$scope.inner = '1234' and removeng-init="inner='1234'and will work.
Another solution would be to use a object in the parent scope containing the variable.
Still does not make sense to me.
What sense does have a scope without a controller?
What practical use have these new child scope?
This is my example.
var app = angular.module('app', []);
app.controller('ctrl', ['$scope', function($scope) {
$scope.result = "Result";
$scope.outer = "outer";
//$scope.inner = "1234";
$scope.test1 = function() {
if ($scope.inner) {
$scope.result = $scope.inner;
} else {
alert("inner is not accesible");
}
}
$scope.test2 = function() {
if ($scope.outer) {
$scope.result = $scope.outer;
} else {
alert("inner2 is not accesible");
}
}
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl" >
<script type="text/ng-template" id="/tpl.html">
<input type="text" ng-init="inner='Inner'" ng-model="inner"></input>
<button ng-click="test1()">TEST1</button>
</script>
<div>
<ng-include src="'/tpl.html'"></ng-include>
<br/>
<input type="text" ng-model="outer"></input>
<button ng-click="test2()">TEST2</button>
<p>{{result}}</p>
</div>
</div>
First you need to understand that scopes and controllers are two separate concepts.
The scope is an object that refers to your application model while a controller is a constructor function that you use to manipulate the scope.
So, "from an Angular's point of view", it's perfectly acceptable to have a scope that is not augmented by a controller.
The idea for creating new child scopes is to have a logical way to separate the application's model. Could you imagine having only one scope for your entire application? You would have to be very careful not to override functions or properties while manipulating the scope in your controllers. Since child scopes prototypically inherit from their parent scope you don't have to worry about that.
One practical example of the usability of these child scopes is, for example, when you have two ng-repeat directives side-by-side, "under" the same scope. If they didn't create their own child scopes, how would you have access to the $index, $first, $last, etc... properties from each of the ng-repeat directives? Without child scopes both would be polluting the "parent" scope with the same properties, overriding each other.
You can read more information on scopes here and on controllers here.
Specifically for ngInclude this is by design: In many cases you want the included content to be isolated.
A scope really does make little sense if there is no js code that works with it, but that code may be in a controller or link function or (as in the case with ngInclude) a postLink function.
Also see How to include one partials into other without creating a new scope? which is almost a duplicate and has a workaround.

How to keep isolate scopes in sync with parent scope variable?

I have a controller:
MyController
- $scope.value = 5
- $scope.list = [
{item: Apple, cost: 5},
{item: Bannana, cost: 2}
]
In my index.html I have an ng-repeat on:
<div ng-controller="MyController">
<mydirective ng-repeat="item in list" value="value"></mydirective>
</div>
In my directive, I have an isolate scope:
{
value:"="
}
In the linking function of the directive I have an bind to an event:
- Onclick, increment scope.value by "1"
The issue is it appears that despite the "=", each of the 2 directives generated from ng-repeat are merely "copies" of the actual MyController "value" variable. How do I make it such that they are linked together so that when I click, the $scope.value of myController gets updated and all the directive "scope.value" would match it? Or is this not possible?
I want to be able to watch on $scope.value of MyController from all the directives so that each of the directives can do something based on "value".
I suppose you hit the problem of your scopes inheriting primitive types.
Try using an object instead:
MyController
- $scope.valueObject = { value: 5 }
then increment valueObject.value by "1"
Each scope of your ng-repeat inherits from the controllers scope.
If you inherit a primitive type like your "value", then changing it in the child scope will only affect this child as it shadows the parent.
If you inherit an object instead, then changing the value would affect the inherited object = the value would be the same for all ng-repeat scopes.
The concept is described here: Understanding scopes
a similar SO question was answered here: Scope inheritance in angularjs

AngularJS: $watch from controller for changes in properties created with ng-repeat

How do you listen in an angular controller to changes in a child properties? And from that controller know which child was changed?
HTML
<div ng-app ng-init='people = [{name: "bill", age: "11"}, {name: "jim", age: ""}, {name: "ryan", age: ""}]'>
<h1> msg: {{ msg }} </h1>
<table>
<tr ng-repeat='person in people'>
<td>
{{ person.name }}
<input ng-model='person.name'>
</td>
</tr>
</table>
</div>
edit
For instance, if I wanted to add .highlight class to every <td> that has a value for age how would I do that? I am new to Angular, but I think $watch would be the right way to do this? Because the value might be changed in the UI but changes could come from elsewhere.
The parent controller would need the value changed, and pinpoint which one changed and add the class.
when deep $watch is needed, but not for the entire object, you can strip out irrelevant data, this way you make the comparison much faster.
Example: (solution by Karl Seamon)
$scope.$watch(function($scope) {
return $scope.people.
map(function(obj) {
return obj.name
});
}, function (newVal) {
$scope.msg = 'person name was changed';
}, true);
Live working example: http://jsfiddle.net/choroshin/uhrQ4/
For more information take a look at my blog post.
You can easily apply a class based on the person's age as follows:
<td ng-class ='{highlight : person.age}'>
Angular will do the magic for you, no need to manually watch for changes.
See http://jsfiddle.net/GAQvM/1/
EDIT: Old answer on $scope.$watchCollection:
It does work when you spell out the actual items to watch:
$scope.$watchCollection('[people[0].name, people[1].name, people[2].name]',
changeHappened);
http://jsfiddle.net/rCPvD/
I realise that this is not exactly feasible for large collections, but I think it points out that the watch is shallow, only one level deep.
The documentation for $watchCollection says (my emphasis):
Shallow watches the properties of an object and fires whenever any of
the properties change (for arrays, this implies watching the array
items; for object maps, this implies watching the properties)

Preserving Scope in ng-repeat ( not wanting child scope )

I might be missing something conceptually but I understand that ng-repeat creates child scopes but for my scenario this is undesirable. Here is the scenario. I have a 3way bind to a firebase dataset. The object is an object with n sub objects. In my current code structure I use ng-repeat to iterate and render these objects with a custom directive. The issue is that these objects are meant to be "live" ( meaning that they are 3-way bound. The highest level object is bound with angularfire $bind ).
So the simple scenario in my case would be where the ng-repeat created scope was not isolated from the scope that it was created from.
I am looking for ideas on how to do this? Or suggestions on other approaches.
This won't be a complete answer, but I can help with the angularFire portion, and probably an angular guru can fill in the blanks for you (see //todo).
First of all, don't try to share scope. Simple pass the variables you want into the child scope. Since you'll want a 3-way binding, you can use & to call a method on the parent scope.
For example, to set up this pattern:
<div ng-repeat="(key,widget) in widgets">
<data-widget bound-widget="getBoundWidget(key)"/>
</div>
You could set up your directive like this:
.directive('dataWidget', function() {
return {
scope: {
boundWidget: '&boundWidget'
},
/* your directive here */
//todo presumably you want to use controller: ... here
}
});
Where &boundWidget invokes a method in the parent $scope like so:
.controller('ParentController', function($scope, $firebase) {
$scope.widgets = $firebase(/*...*/);
$scope.getBoundWidget = function(key) {
var ref = $scope.widgets.$child( key );
// todo, reference $scope.boundWidget in the directive??
ref.$bind(/*** ??? ***/);
};
});
Now you just need someone to fill in the //todo parts!
You still have access to the parent scope in the repeat. You just have to use $parent.
Controller
app.controller('MainCtrl', ['$scope', function ($scope) {
$scope.someParentScopeVariable = 'Blah'
$scope.data = [];
$scope.data.push({name:"fred"});
$scope.data.push({name:"frank"});
$scope.data.push({name:"flo"});
$scope.data.push({name:"francis"});
}]);
HTML
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="item in data | filter: search">
<td>
<input type="text" ng-model="$parent.someParentScopeVariable"/>
<input type="text" ng-model="item.name">
</td>
</tr>
</table>
</body>
Plunker

Resources