Angular X-Editable - additional parameters to onbeforesave - angularjs

I am using angular-xeditable to edit a table value inline and want to save that value in the database once the input is blurred. What I want to do is call the save function using the "onbeforesave" attribute.
The problem is the function takes only the changed value as parameter. I also need the id of the tag on which xeditable is applied.
So how can I pass a reference to the DOM element (best option) or just the element id (will suffice in this case).
Many thanks!

on your ng-repeat on front end, on your "onBeforeSave" attribute you can pass the $index so it will be like onBeforesave="myFUnction($index)" and then use the index in targeting the element of array to alter values. so if your front end ng-repeat uses $scope.items, you will use the index to target $scope.items[index] and then you can take all the data on this index and pass data to server to do some database updates.

Keep in mind that you can pass the repeated object, it can help when you'll try to figure out the object from that index,
As explained here :
How to use Angular-Xeditable's onBeforeSave / onAfterSave methods with more than $data as parameter
<div ng-repeat="p in people">
{{p.name}}
</div>
You'd then have the following code in your controller:
$scope.updatePerson = function(person) {
var ret = PersonService.save(person); // or whatever save mechanism you use
// You need to return a error string if save fails, true otherwise
if (ret !== null) { // You have to make sure this logic works for your save service
return ret;
}
return true;
}
I prefer this solution because its avoiding index + filtering problems
as explained here:
http://codeutopia.net/blog/2014/11/10/angularjs-best-practices-avoid-using-ng-repeats-index/

Related

Changing variable without influencing view

I am displaying a view (ui-router) on the Ionic platform with a message list. New messages should be shown as unread, but then corresponding variables should be changed to read. The problem is that when I set the controller to change the proper variable, the view automatically updates itself immediately with this change and the messages are immediately displayed as read.
How can I deal with this?
In other words: in a variable-dependent view, I want to display the view first and then modify the variable, so that the change of the variable does not affect the view.
The current code (MarkAsRead() is called from within the template):
$scope.messages = $rootScope.Messages.Inbox;
$scope.MarkAsRead = function(ID_NR)
{
angular.forEach($rootScope.Messages.Inbox, function (value, key) {
if ((value.IsRead == 2) && (value.From == ID_NR))
{
value.IsRead = 3;
}
});
}
It's odd you're battling the main thing angular is known for: two-way binding. That said, if you really want to make changes to the variable without updating the view, you can use angular.copy to make a deep copy of the object and manipulate the copy itself. Or display the copy but manipulate the original
Another option, if you're using Angular > 1.3, you can use one-time binding to only update the view first time:
Simply prefix your expression with :: e.g
<p>{{::message.IsRead}}</p>
https://docs.angularjs.org/guide/expression#one-time-binding

AngularJS typeahead select on blur

I'm using typeahead through in my AngularJS project and I would like to have it select the entry if I type the full value and click out of the field.
I've put together an example of what I mean
http://plnkr.co/edit/NI4DZSXofZWdQvz0Y0z0?p=preview
<input class='typeahead' type="text" sf-typeahead options="exampleOptions" datasets="numbersDataset" ng-model="selectedNumber">
If I type in 'two' and click on 'two' from the drop down then I get the full object {id: 2, name: 'two'}. This is good, if however I type 'two' and click to the next field without selecting is there a way to accept the top of the list on loss of focus on a text field?
I'm not sure if I'd want to have that sort of functionality in my app. The user hasn't actually selected anything. So selecting something for them would introduce frustrations.
But I do understand that often odd requirements are needed. In this case, I'd attack it using ngBlur. Assign a function to be called on blur. You can grab the contents of ng-model and then loop through your data (assuming static & not being sent via server) to find a match.
You can most likely just look at the source code of your typeahead directive and strip out the part does the comparison and then choose the first item in the array.
Unfortunately the underlying component does not emit any events for this condition. This will make the solution more complex. However when the value is being entered and the Typehead magic has happened you can supplement those events and catch them to update your ngModel.
I have created a plnkr based on your plnkr and although have not cleaned up but it is a working plnkr doing by far what you need.
The gist of this is following code however you can put this code wherever best suited
The explanation below:
//Crux - this gets you the Typeahead object
var typeahead = element.data('ttTypeahead');
//This gets you the first
var datum = typeahead.dropdown.getDatumForTopSuggestion();
if (datum){
//you can do lot of things here however
//..I tried to - fill in the functionality best suited to be provided by Typeahead
//for your use case. In future if Typeahead gets this
//..feature you could remove this code
typeahead.eventBus.trigger("hasselections", datum.raw, datum.datasetName);
}
In the above code you can also save the datum somewhere in the scope for doing whatever you like with it later. This is essentially your object {num: 'Six'} Then you may also use ngBlur to set it somewhere (however the plnkr I created doe snot need these gimmicks.)
Then further down - ngModel's value is set as below
element.bind('typeahead:hasselections', function(object, suggestion, dataset) {
$timeout(function(){
ngModel.$setViewValue(suggestion);
}, 1);
//scope.$emit('typeahead:hasselections', suggestion, dataset);
});
I'm with EnigmaRM in that ngBlur seems to be the way to do what you want. However, I agree with the others that this could be somewhat strange for the end users. My implementation is below (and in plnkr). Note that I trigger on ngBlur, but only apply the model if and only if there is only one match from Bloodhound and the match is exact. I think this is probably the best of both worlds, and hope it should give you enough to go on.
$scope.validateValue = function() {
typedValue = $scope.selectedNumber;
if(typedValue.num !== undefined && typedValue.num !== null)
{
return;
}
numbers.get(typedValue, function(suggestions) {
if(suggestions.length == 1 && suggestions[0].num === typedValue) {
$scope.selectedNumber = suggestions[0];
}
});
};

loading array values in custom order using ng-repeat

Loading array initial value/1st value to end of the array; using ng-repeat and orderby filter;
Array using ng-repeat
In your script.js file, create a sort function.
$scope.sort = function(value){
var index = $scope.myvar.indexOf(value);
if(index === 0){
return $scope.myvar.length;
}
return index;
}
In your index.html file, change your ng-repeat attribute to look like this:
<li ng-repeat="v in myvar|orderBy:sort:false">{{v}}</li>
A couple of things to note. This assumes that you want the natural order of the array except that you want the first element to go to the end. If you want a different order you will need to alter the sort function. See the AngularJS orderBy function documentation for more details.
Also, the indexOf function of an array is not well supported across all browsers (IE does not implement it if I am not mistaken). You will need to create it yourself if it doesn't already exist. See here for help on this topic.
Click here to see a working plunker version.

Angular filter works but causes "10 $digest iterations reached"

I receive data from my back end server structured like this:
{
name : "Mc Feast",
owner : "Mc Donalds"
},
{
name : "Royale with cheese",
owner : "Mc Donalds"
},
{
name : "Whopper",
owner : "Burger King"
}
For my view I would like to "invert" the list. I.e. I want to list each owner, and for that owner list all hamburgers. I can achieve this by using the underscorejs function groupBy in a filter which I then use in with the ng-repeat directive:
JS:
app.filter("ownerGrouping", function() {
return function(collection) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}
});
HTML:
<li ng-repeat="(owner, hamburgerList) in hamburgers | ownerGrouping">
{{owner}}:
<ul>
<li ng-repeat="burger in hamburgerList | orderBy : 'name'">{{burger.name}}</li>
</ul>
</li>
This works as expected but I get an enormous error stack trace when the list is rendered with the error message "10 $digest iterations reached". I have a hard time seeing how my code creates an infinite loop which is implied by this message. Does any one know why?
Here is a link to a plunk with the code: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview
This happens because _.groupBy returns a collection of new objects every time it runs. Angular's ngRepeat doesn't realize that those objects are equal because ngRepeat tracks them by identity. New object leads to new identity. This makes Angular think that something has changed since the last check, which means that Angular should run another check (aka digest). The next digest ends up getting yet another new set of objects, and so another digest is triggered. The repeats until Angular gives up.
One easy way to get rid of the error is to make sure your filter returns the same collection of objects every time (unless of course it has changed). You can do this very easily with underscore by using _.memoize. Just wrap the filter function in memoize:
app.filter("ownerGrouping", function() {
return _.memoize(function(collection, field) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}, function resolver(collection, field) {
return collection.length + field;
})
});
A resolver function is required if you plan to use different field values for your filters. In the example above, the length of the array is used. A better be to reduce the collection to a unique md5 hash string.
See plunker fork here. Memoize will remember the result of a specific input and return the same object if the input is the same as before. If the values change frequently though then you should check if _.memoize discards old results to avoid a memory leak over time.
Investigating a bit further I see that ngRepeat supports an extended syntax ... track by EXPRESSION, which might be helpful somehow by allowing you to tell Angular to look at the owner of the restaurants instead of the identity of the objects. This would be an alternative to the memoization trick above, though I couldn't manage to test it in the plunker (possibly old version of Angular from before track by was implemented?).
Okay, I think I figured it out. Start by taking a look at the source code for ngRepeat. Notice line 199: This is where we set up watches on the array/object we are repeating over, so that if it or its elements change a digest cycle will be triggered:
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
Now we need to find the definition of $watchCollection, which begins on line 360 of rootScope.js. This function is passed in our array or object expression, which in our case is hamburgers | ownerGrouping. On line 365 that string expression is turned into a function using the $parse service, a function which will be invoked later, and every time this watcher runs:
var objGetter = $parse(obj);
That new function, which will evaluate our filter and get the resulting array, is invoked just a few lines down:
newValue = objGetter(self);
So newValue holds the result of our filtered data, after groupBy has been applied.
Next scroll down to line 408 and take a look at this code:
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
The first time running, oldValue is just an empty array (set up above as "internalArray"), so a change will be detected. However, each of its elements will be set to the corresponding element of newValue, so that we expect the next time it runs everything should match and no change will be detected. So when everything is working normally this code will be run twice. Once for the setup, which detects a change from the initial null state, and then once again, because the detected change forces a new digest cycle to run. In the normal case no changes will be detected during this 2nd run, because at that point (oldValue[i] !== newValue[i]) will be false for all i. This is why you were seeing 2 console.log outputs in your working example.
But in your failing case, your filter code is generating a new array with new elments every time it's run. While this new array's elments have the same value as the old array's elements (it's a perfect copy), they are not the same actual elements. That is, they refer to different objects in memory that simply happen to have the same properties and values. Hence in your case oldValue[i] !== newValue[i] will always be true, for the same reason that, eg, {x: 1} !== {x: 1} is always true. And a change will always be detected.
So the essential problem is that your filter is creating a new copy of the array every time it's run, consisting of new elements that are copies of the original array's elments. So the watcher setup by ngRepeat just gets stuck in what is essentially an infinite recursive loop, always detecting a change and triggering a new digest cycle.
Here's a simpler version of your code that recreates the same problem: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview
The problem vanishes if the filter stops creating a new array every time it's run.
New to AngularJS 1.2 is a "track-by" option for the ng-repeat directive. You can use it to help Angular recognize that different object instances should really be considered the same object.
ng-repeat="student in students track by student.id"
This will help unconfuse Angular in cases like yours where you're using Underscore to do heavyweight slicing and dicing, producing new objects instead of merely filtering them.
Thanks for the memoize solution, it works fine.
However, _.memoize uses the first passed parameter as the default key for its cache. This could not be handy, especially if the first parameter will always be the same reference. Hopefully, this behavior is configurable via the resolver parameter.
In the example below, the first parameter will always be the same array, and the second one a string representing on which field it should be grouped by:
return _.memoize(function(collection, field) {
return _.groupBy(collection, field);
}, function resolver(collection, field) {
return collection.length + field;
});
Pardon the brevity, but try ng-init="thing = (array | fn:arg)" and use thing in your ng-repeat. Works for me but this is a broad issue.
I am not sure why this error is coming but, logically the filter function gets called for each element for the array.
In your case the filter function that you have created returns a function which should only be called when the array is updated, not for each element of the array. The result returned by the function can then be bounded to html.
I have forked the plunker and have created my own implementation of it here http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn
It does not use any filter. The basic idea is to call the groupBy at the start and whenever an element is added
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
$scope.addBurger = function() {
hamburgers.push({
name : "Mc Fish",
owner :"Mc Donalds"
});
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
}
For what it's worth, to add one more example and solution, I had a simple filter like this:
.filter('paragraphs', function () {
return function (text) {
return text.split(/\n\n/g);
}
})
with:
<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>
which caused the described infinite recursion in $digest. Was easily fixed with:
<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>
This is also necessary since ngRepeat paradoxically doesn't like repeaters, i.e. "foo\n\nfoo" would cause an error because of two identical paragraphs. This solution may not be appropriate if the contents of the paragraphs are actually changing and it's important that they keep getting digested, but in my case this isn't an issue.

Linking MVC In AngularJS

I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).

Resources