Dynamic form controls with ng-repeat Angular - arrays

In my controller:
$scope.mytest = [1,2,3];
My view:
<div ng-repeat="foo in mytest">
{{foo}}
<input type="text" ng-model="mytest[$index]" name="$index" />
</div>
{{ mytest | json }}
When I load the page it renders this:
I can change the value, and the binding updates all the values:
The only problem, is the form field loses focus. When ng-model updates the scope's values, it causes ng-repeat to manipulate the dom, and the fields lose focus after each key press. I have to click back inside the field between each keypress to enter a value with length more than 1 character.
What I'm trying to accomplish here is to render a text field for each value of an array. Updating the text field should update the scope values, and also update all places I'm displaying those values within the view, both inside & outside of the ng-repeat

Figured it out. I changed my array to be an array of objects with IDs:
$scope.mytest = [{id:1, title:'one'}, {id:2, title:'two'}];
Then I prevented ng-repeat from removing & re-inserting a dom node when it changes:
<div ng-repeat="foo in mytest track by foo.id">
My previous attempt from the original post fails. In this example illustrating why, you can see that Angular can't discern what changes were made:
[1,2,3] // point A
[2,2,3] // point B
Angular has no way of knowing what took place to get from point A to B. Did I edit the first value? Remove it & and insert a new value? Make multiple edits that cancel each other out?
In this example, Angular is able to discern what happened to get from Point A to B:
$scope.mytest = [{id:1, title:'one'}, {id:2, title:'two'}]; // point A
$scope.mytest = [{id:1, title:'two'}, {id:2, title:'two'}]; // point B
In this example I use the "value object" design pattern. I wrap my scalar values in an object which has an identifier that is immutable, while the wrapped value remains mutable.
Combined with the "track by" syntax of ng-repeat, it allows Angular to discern between an add & subsequent removal versus an edit. ng-repeat can then directly update the bindings in the isolate scope, as opposed to redrawing the whole darn page.

Related

Binding behavior of ng-repeat track by

Lets assume I am binding one big nested object to the $scope of the view shown in the code. Now, the value of an "e" object is updated. This would cause angular the check all bindings and update the DOM. If I used "track by" instead, in each ng-repeat directive, would that mean that only the binding for the "e" object would react and the dom for the "e" object be updated?
<div ng-repeat="a in b">
<div ng-repeat="c in a">
<div ng-repeat="d in c">
<div ng-repeat="e in d">
{{e.value}}<br>
</div>
</div>
</div>
</div>
The bindings will be checked no matter what, and updated only if different, per the digest cycle. As for re-building the DOM elements, Angular uses unique identifiers to determine whether each item in an ng-repeat already has a matching DOM element, or if it needs to render a new one.
By default, Angular creates and manages these unique identifiers under the hood, using the $id of each object (or $$hashKey).
track by was added later, as a way to tell Angular to use a unique identifier of your choice, rather than managing it under the hood.
This is useful when updating the data removes/changes the $id or $$hashKey, triggering unnecessary re-builds of each DOM element, even when the data didn't change at all.
Consider this example:
You have an ngRepeat which displays data records:
<li ng-repeat="item in data">{{item.value}}</li>
You use a service DataService to update your data, which has a fetch() method which retrieves data from an SQL database, and returns the records.
Updating the data in your $scope involves calling that service, and re-assigning your data variable to the result:
$scope.data = DataService.fetch();
That means, even if only one item was different, all the $id or $$hashKey properties are gone or different, and Angular will assume all items are new. It will re-build all the DOM elements from scratch.
However, since your data is from an SQL database, you already have a unique identifier (primary key), the id column. You could then change your ngRepeat to be:
<li ng-repeat="item in data track by item.id">{{item.value}}</li>
Now, instead of looking for $$hashKey, which gets lost every time you re-assign the data, Angular will use the property you told it to (item.id). Since that property does persist across re-assigning the variable, the list is once again optimized, because Angular will only re-build DOM elements for new items.

AngularJS re-rendering in ng-repeat inner working

The following code contain a input box, a radio button and a button.
When the button is clicked, it will generate one more inputbox and radio button. All the radio button are under the same name.
<ul ng-repeat="i in addQuestion.loop(addQuestion.numOfChoice) track by $index">
<li>
answers:<input type="text"/>
correct:<input type="radio" name="correctChoice" value ={{$index}}/>
</li>
</ul>
<button ng-Click="addQuestion.numOfChoice = addQuestion.numOfChoice + 1">add more choices</button><br/><br/>
//controller:
$scope.addQuestion = {
numOfChoice: 1,
loop: function(num){
return new Array(num);
}
}
My question is while I successfully made this work the way I wanted.
I have no idea how the 'magic' work on the re-rendering whenever a numOfChoice get incremented.
Two questions:
In the ng-repeat it calls the function loop that takes in a parameter: how does the change of the argument trigger a re-render, causing the loop to run again. I would understand if it is a variable.
Whenever I click "add more", it will render one more inputbox and radio button, I don't understand how the states of the previous rendered inputbox/box stay there. In something like reactJS, it will re-render the whole thing, and all the state is lost unless I store it somewhere. How does it store all the state while re-rending the whole ng-repeat loop. Or does it not re-run the ng-repeat but do something else? does it have something to do with the index?
Hope I was clear on my question, please let me know.
The function loop returns a new object which ngRepeat was tracking. So when the value of ng-repeat loop variable changes, it triggers the new rendering.
ngRepeat keeps track of all items in the collection and their corresponding DOM elements. So if the item already exists for example, it will not re-render.

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

Can't I use one of the members of my scope collection as a default for radio button?

Let's say I have this controller:
myCtrl = function($scope)
{
$scope.shipMethods = [{ "name": "standard",
"id" : 1},
{ "name": "overnight",
"id" : 2},
{ "name": "next day",
"id" : 3}
];
$scope.selectedShipMethod = $scope.shipMethods[0];
//works: $scope.selectedShipMethod = { "name": "standard", "id" : 1};
};
And I've got this in my view:
<div ng-app>
<div ng-controller="myCtrl">
<form>
<h1>you selected {{ selectedShipMethod.name }} </h1>
<label class="radio-input" data-ng-repeat="shipMethod in shipMethods">
<b class="title">{{shipMethod.name}}</b>
<input type="radio" name="ship-method" value="{{shipMethod.name}}"
data-ng-model="selectedShipMethod.name" />
</label>
</form>
</div>
I'm trying to set a selectedShipMethod when the user selects one of the radio buttons. But when I select either the second or third radio buttons, the label next to the first one gets updated with whatever is in selectedShipMethod.name. But here's the ng-mystery: If I make a copy of the first item in my array and initialize $scope.selectedShipMethod to that copy, I do NOT see the issue.
It looks like there's something deeper to be understood here about radio buttons in an ng-repeat. The actual data I'm working with will be coming from the web server has some estimated shipping dates which will change, so I'd really like to use a member of the array as the default.
Here's a fiddle
When you click a radio button, the first item in the radio button list changes - as does the corresponding label: {{ selectedShipMethod.name }}.
To understand why this is happening, we need to take a closer look at which model the radio button is actually bound to. In the parent controller, you have initialized the selectedShipMethod to the first ship method ($scope.ShipMethods[0]) in parent scope:
// bind the selectedShipMethod to the first ship method
$scope.selectedShipMethod = $scope.ShipMethods[0];
This is important to remember, because the selectedShipMethod never actually changes - even when a different radio button is clicked.
To see why, take a closer look at the ng-model of the radio button:
<input type="radio" name="ship-method"
data-ng-model="selectedShipMethod.name" />
The ng-model that the radio button is bound to is actually 'selectedShipMethod.name'. Notice there is a '.' in the middle of the model. According to scope inheritance rules, 'selectedShipMethod' will be resolved first by searching for the declaration of the variable in the current scope; if it is not found in the current scope, it will search for it in the parent scope, etc, until it either finds the scope variable or stops at $rootScope. In this example, it will find it in the parent scope, which if you remember, is bound to the first ship method.
By selecting a different radio button, you are actually changing the '.name' property of the first shipping method! This is obviously not what we want to happen.
So what is the solution?
The solution is simple - change the binding so that the selectedShipMethod is bound to the same model that is on parent scope:
<input type="radio" name="ship-method" ng-value="shipMethod"
data-ng-model="$parent.selectedShipMethod" />
That is exactly what we are doing when we do '$parent.selectedShipMethod'. $parent refers to the parent scope, so according to scope inheritance rules, it will find and resolve the selectedShipMethod variable in the parent scope.
You might be asking yourself at this point, is the $parent prefix really necessary? The answer is Yes. Since the scope variable is being set (when the radio button is clicked), the setter for the scope variable is called. There is no scope inheritance resolution happening if there is no '.' in the model and a setter is used - without the $parent prefix, it would create a copy of the scope variable in child scope that over-shadows the scope variable in parent scope, effectively breaking the model binding.
Finally, we use ng-value since shipMethod is a model from a ng-repeat directive. When a radio button is selected, $parent.selectedShipMethod is set to the selected shipMethod. Everything works because the right model ($parent.selectedShipMethod) is being bound to the right scope - the parent scope.
I hope all this makes sense. Here is a Working Fiddle
Here you go. Instead of setting the selected object to the first element of array, just make a copy into a new object.
http://jsfiddle.net/csrow/XFq56/3/
When you set
a = b[0];
a and b[0] are both pointing to the same object. So, when you are setting 'a' with the radio button, you are actually changing the object b[0].
The problem in your code is Reverse Binding. Try the following code:
$scope.selectedShipMethod = angular.copy($scope.shipMethods[0]);

Angularjs function call issue with nested elements

I have an angulars setup as follows, trying to mimic some excel functionality where I have a controller nested inside an ng-repeat.
<tr ng-repeat="lw in lw_list" my-lw ng-model="lw"
<td>
<!-- next two elements act as an excel cell, one for inputing data, they other for displaying calcualtion result -->
<div ng-controller="MyCellCtrl">
<input type="text" class="inputdiv" ng-model="lw.library.name" >in</input>
<div class="output" ng-bind="getCellValue(lw.library.name)" syle="postion:absolute" contenteditable="True" >out</div>
</div>
<div ng-controller="MyCellCtrl">
more input / div pairs to act as a new cell
.....
</div>
</td>
I have the stylesheets set up so that input and output are in the same position, and get hidden / unhidden, so that they act like an excel cell (you type a formula, then when you leave focus, it updates the content).
Anyway, when I put a console.log() inside the getCellValue() function, to show what instance of the controller is being called, then typing in one particular cell, I can see that getCellValue() is being called on every cell.
Is there some way to call getCellValue() when the input is updated without calling the method on every instance?
(I based this code on the code from this tutorial:
https://github.com/graunked/spreadsheet
you can see the same behaviour by putting a console.log in the compute function. If you increase the arrays to 20 x 20 elements, it starts to get slow when you type anything.)
Is there some way to call getCellValue() when the input is updated without calling the method on every instance?
<div class="output" ng-bind="foo">
then use $watch:
function MyCellCtrl($scope)
{
$scope.foo = $scope.lw.library.name;
$scope.$watch('foo', function(newValue) {
$scope.foo = getCellValue($scope.foo);
});
}
or use viewChangeListeners as an alternative:
function MyCellCtrl($scope)
{
$scope.foo = $scope.lw.library.name;
this.$viewChangeListeners.push(function(newValue) {
$scope.foo = getCellValue($scope.foo);
});
}
References
Effective Strategies for avoiding watches in AngularJS
Compile, Pre, and Post Linking in AngularJS

Resources