Object changes inside array but stays unchanged on page - angularjs

I have a table which is made by ng-repeat. That part which doesnt get refreshed is below;
<tbody class="defTbody" ng-repeat="activity in activityList track by activity.ActivityID">
<tr class="{{activity.DetailExpanded == true ? 'shown' : ''}}">
<td>
<a ng-if="!activity.DetailExpanded" ng-click="activity.DetailExpanded = true"><i class="fa fa-fw fa-plus-circle txt-color-green fa-lg"></i></a>
<a ng-if="activity.DetailExpanded" ng-click="activity.DetailExpanded = false"><i class="fa fa-fw fa-minus-circle txt-color-red fa-lg"></i></a>
</td>
<td style="font-size:smaller">
<a ng-click="getActivityStatements(activity.ActivityID)" style="cursor: pointer;">
{{activity.PlanDateTime}} / {{activity.ActivityType_DisplayName}}
<br />
<small class="text-muted"><i>{{activity.ActivityID}}<i></i></i></small>
</a>
</td>
<td style="font-size:smaller">
{{activity.ParentActivityDisplayName}}
<br />
<small class="text-muted"><i>{{activity.ParentActivityID}}<i></i></i></small>
</td>
<td>{{activity.Customer_DisplayName}}</td>
<td>{{activity.ActivityStatus_DisplayName}}</td>
<td>
{{activity.StatusReason}}
<br />
{{activity.StatusReasonNote}}
</td>
</tr>
</tbody>
$scope.activityList is fine, it gets populated correctly. This is how I change an item in activiyList
$scope.UpdateActivityListItem = function (activityID, prevResponseDetail) {
var url = "/api/activity/GetActivityResponse?activityID=" + activityID;
$http({
method: 'GET',
url: url
}).then(function successCallback(result) {
var response = result.data;
if (response.Status == "SUCCESS") {
var activity = $scope.activityList.filter(activity => activity.ActivityID == activityID)[0];
var renewedAct = response.Detail[0];
activity = renewedAct;
$scope.$apply();
console.log("yenilenmiş aktivite:" +$scope.activityList)
$('#modalActivityDetail').modal('hide');
console.log(JSON.stringify($scope.activityList));
$scope.addSpinnerClass(spinner);
}
else {
console.log("fail");
}
},
function errorCallback(result) {
console.log("error");
}
);
};
I confirmed in debug that item gets "updated" nicely but in screen it stays same.
I thought this was kind of a problem whicj $scope.$apply() can solve but adding it (as shown in code piece above) caused this problem
Error: error:inprog
Action Already In Progress
$digest already in progress
What else can I do?
PS: This is written in Angular JS 1.0

check this, You are filtering activityList and updating a filtered array (result) first element not activityList array.
var renewedAct = response.Detail[0];
for (let index = 0; index < $scope.activityList.length; index++) {
const activity = $scope.activityList[index];
if (activity.ActivityID == activityID) {
$scope.activityList[index] = renewedAct;
break;
}
}

You can do that even without looping again using splice with a single line
Here is the code,
var activity = $scope.activityList.filter(activity => activity.ActivityID == activityID)[0];
var index = $scope.activityList.indexOf(activity);
var renewedAct = response.Detail[0];
activity = renewedAct;
$scope.activityList.splice(index, 1, activity)
Note: The above line removes that element at that place and adds your new element.

Before calling $apply please check weather the digest cycle is busy or not.
if(!$scope.$$phase) {
//$digest or $apply
}
---------EDIT------
A new, powerful method has been added to any $scope: $evalAsync. Basically, it will execute its callback within the current digest cycle if one is occurring, otherwise a new digest cycle will start executing the callback.
Try this:
In these cases and all the others where you had a !$scope.$$phase, be sure to use $scope.$evalAsync( callback ) do all related changes in callback.

Related

getting the updated value of inputs in ng-repeat | Angular Js

I have a inputs in a table filled out with ng-repeat, i want to be able to get the updated values by one click for all inputs.
My View:
<tr ng-repeat="sf in selectedFacture">
// displaying default values in the input
<td><input class="facture_item_name" ng-model="sf.facture_item_name" value="{{sf.facture_item_name}}" type="text"/></td>
<td><input class="fcls_crt" ng-model="sf.fcls_crt" value="{{sf.fcls_crt}}" type="number"/></td>
<td><input class="fpiece" ng-model="sf.fpiece" value="{{sf.fpiece}}" type="number"/></td>
<td colspan="4"><input type="text" class="form-control note" ng-model="sf.note" value="{{sf.note}}"/></td>
<tr>
<td ng-click="updateFacture(sf.id,sf,sf.facture_type,sf.item_id)">SUBMIT</td>
</tr>
</tr>
JS:
// getting new values and send them to server side
$scope.updateFacture=function(id,sf,type,item_id){
var url = '../php/history.php';
var func = "updateFacture";
sf = sf || {};
var editedQuanCls= sf.fcls_crt,
editedQuan_piece= sf.fpiece,
editedQuan_cls_crt_gate= sf.fcls_crt_gate,
editedQuan_piece_gate= sf.fpiece_gate,
editedNote= sf.note;
var data = {"function": func,
"factureId":id,
"item_id":item_id,
"facture_type":facture_type,
"editedQuanCls":editedQuanCls,
"editedQuan_cls_crt_gate":editedQuan_cls_crt_gate,
"editedQuan_piece":editedQuan_piece,
"editedQuan_piece_gate":editedQuan_piece_gate,
"editedNote":editedNote};
var options = {
type : "get",
url : url,
data: data,
dataType: 'json',
async : false,
cache : false,
success : function(response,status) {
alert("success")
},
error:function(request,response,error){
alert("errro: " + error)
}
};
$.ajax(options);
}
I tried to put the updated button in a td aside to the inputs and it works fine, but this will update each row separately, but my need is to updated them all in one click.
I'll attach a screen shot of my view.
Many Thanks in advance
<input class="facture_item_name" ng-model="sf.facture_item_name" value="{{sf.facture_item_name}}" ng-change="updateValues(sf.facture_item_name)" type="text"/>
$scope.updateValues=function(value){
$scope.sf.facture_item_name=value;
}
What you need is a wrapper function.
First add a button on the page that covers the All option like:
<button ng-click="updateAllFacture()">SUBMIT ALL</button>
Then add the wrapper function. All this does is loop through each item in the list and call the update function.
The wrapper function would look like:
$scope.updateAllFacture=function(){
angular.forEach($scope.res, function(sf, index) {
$scope.updateFacture=function(sf.id,sf,sf.facture_type,sf.item_id );
});
};
If you have an awful lot of items then there will be a lot of calls back to your api. Consider submitting all the inputs in the form as a post instead - then there will be just one call back, but you will need to program your controller for that.

ServiceNow spUtil

I'm trying to make a slight improvement to an existing widget that our team created, but can't seem to get it to work correctly. We have a widget that does a RowCount of tasks and groups them by state. I want the RowCount to auto update once a task is complete without having the user press the refresh button. I've read some documentation on $rootscope, $broadcast, and $on, but can't seem to get it to work.
Below is snippet of our HTML:
<table class="table table-sm table-responsive">
<tbody>
<tr class="h3">
<td colspan=2>Complete</td>
</tr>
<tr class="h2 bg-success" ng-repeat="x in data.values track by $index">
<td><span class="glyphicon glyphicon-check"></span></td>
<td>{{x.completedCount}}</td>
</tr>
</tbody>
</table>
A snippet of our Server Script:
var values = [];
var _completedCount;
var gsCompleted = new GlideRecordSecure('sn_hr_core_task');
//CLOSED COMPLETE, CLOSED INCOMPLETE,
gsCompleted.addQuery('state', 'IN', '3,4,7');
gsCompleted.addQuery('assigned_to', gs.getUserID());
gsCompleted.addQuery("parent.state", 'NOT IN', '1,800,900');
gsCompleted.query();
if(gsCompleted){
_completedCount = gsCompleted.getRowCount();
}
else{
_completedCount = 0;
}
values.push(
{
completedCount: _completedCount
});
data.values = values;
How do I get this widget to auto update the Completed row count without refreshing the page? I've been playing around with spUtil recordWatch, but cannot get it to work correctly:
function($scope, $sce, spUtil) {
var c = this;
c.data.loading = true;
//After page initially loads re-call server script to load data
c.server.get({
action: 'retrieve_data'
}).then(function(response) {
c.data.loading = false;
console.log('Response');
console.log(response);
c.data.values = response.data.values;
spUtil.recordWatch($scope, 'sn_hr_core_task', "", function(name,data) {
spUtil.update($scope);
})
});
}
Take a look at the widget Simple List, it has an example of one that may help a bit.
You should be able to change your recordWatch to this
var filter = "stateIN3,4,7^parent.stateNOT IN1,800,900^assigned_to=" + window.NOW.user_id;
spUtil.recordWatch($scope, 'sn_hr_core_task', filter);
You generally won't need a callback function unless there is some specific action you're triggering.

Grails GSP Loop through an index and do somthing with selected lines

In an Index-gsp, I want to be able to select an arbitrary number of lines and then by clicking a link send all those lines to a controller for processing e.g. creating new objects of a different kind.
I've no idea how selection can be done or how to collect these selected lines in a GSP. Maybe I should use a checkbox on each line if that's possible?
It's a list of products which is displayed using a modified index.gsp.
Each product-line has a checkbox in front.
What I want is to make a list of the products that are checked an then transmit this list to a controller.
a part of this index.gsp:
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
<li><g:link class="create" action="createOffer"><g:message code="default.new.label" args="[entityName]" params="toOffer" /></g:link></li>
</ul>
</div>
<div id="list-prodBuffer" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<table>
<thead>
<tr>
<td> Välj</td>
<td> ID</td>
</tr>
</thead>
<tbody>
<g:each in="${prodBufferList}" status="i" var="prodBuffer">
<tr class="${ (i % 2) == 0 ? 'even': 'odd'}">
<td><g:checkBox name="toOffer" value="${prodBuffer.id}" checked="false" /></td>
<td>${prodBuffer.id}</td>
So this not an ordinary form, just a list where I want to use a link to transmit it to the controller.
I'm a beginner and have no idea how to do it.
You can collect all necessary data from page using javascript, and then send all data to your controller for processing.
There are a lot of ways to do it.
For example send via JQuery:
<script>
//some code
var items = [1,2,3];
//some code
$('#add-location').click(function () {
$.ajax({
type: "POST",
url: "${g.createLink(controller:'myController', action: 'myControllerMethod')}",
data: {items: items},
success: function (data) {
console.log(data)
}
});
});
</script>
I will answer this but have to slow down since it feels like i am beginning to write your project:
In gsp you will need to have a hidden field followed by a check box amongst data you are trying to capture, checkbox should contain all the data elements required to build your output.
<g:hiddenField name="userSelection" value=""/>
<g:checkBox name="myCheckBox" id='myCheckBox' value="${instance.id}"
data-field1="${instance.field1}" data-field1="${instance.field1}"
checked="${instance.userSelected?.contains(instance.id)?true:false}" />
In the java script segment of the page you will need to add the following
This will then auto select selection and add to javascript array
// Customized collection of elements used by both selection and search form
$.fn.serializeObject = function() {
if ($("[name='myCheckBox']:checked").size()>0) {
var data=[]
$("[name='myCheckBox']:checked").each(function() {
var field1=$(this).data('field1');
var field2=$(this).data('field2');
data.push({id: this.value, field1:field1, field2:field2 });
});
return data
}
};
Most importantly will your data sit across many different gsp listing pages if so you will need to hack pagination:
//Modify pagination now to capture
$(".pagination a").click(function() {
var currentUrl=$(this).attr('href');
var parsedUrl=$(this).attr('href', currentUrl.replace(/\&userSelection=.*&/, '&').replace(/\&userSelection=\&/, '&'));
var newUrl=parsedUrl.attr('href') + '&userSelection=' + encodeURIComponent($('#userSelection').val());
window.location.href=newUrl
return false;
});
Then in the controller parse the JSON form field and make it into what you want when posted
def u=[]
def m=[:]
if (params.userSelection) {
def item=JSON.parse(params.userSelection)
item?.each {JSONObject i->
// When field1 is null in JSON set it as null properly
if (JSONObject.NULL.equals(i.field1)) {
i.field1=null
}
if (resultsGroup) {
if (!resultsGroup.contains(i.id as Long)) {
u << i
}
} else {
u << i
}
}
m.userSelected=item?.collect{it.id as Long}
m.results=u
}
return m

Slow reaction to button click in NgRepeat list of 1000 items

I have a list of 1000+ items which I display using NgRepeat in Angular 1.3. The list populates with buttons. I have noticed significant delay on the click event int he list once it grows in size. When the list is only 5-10 items the clicks are instant. When the list is 1000 there is about 2-5 second delay before the button clicks are actually processed.
Now I cannot tell if this is a browser issue, but I suspect it has to do with too many listeners being used somewhere, causing the browser to check for them.
Here is sample of code in case there is a culprit hiding in there:
<div id="side" class="animated" style="min-height: 250px;"
data-ng-class="{'fadeInRight':documentDone}" data-ng-style="settings.listCss">
<div class="col-md-12 text-center" data-ng-style="settings.listCss"><h4>{{label}}</h4> {{inSide}} </div>
<div data-ng-repeat="doc in ::documents track by $index" id="{{ ::doc.id }}"
class="document ng-hide" data-ng-show="doc.show"
data-ng-init="docSettings = (settingslist[doc.companyid] || settings)" data-ng-style="::docSettings.listCss">
<div class="col-md-12" data-ng-style="docSettings.listCss">
<h4>
<span>{{ ::$index + 1 }}</span>
<span class="title-in-clusters">
{{ ::doc.title }}
<button type="button"
class="btn btn-primary btn-xs"
data-ng-click="viewJob(doc, docSettings)"
data-ng-style="docSettings.buttonCss">
<strong>VIEW</strong>
</button>
<a href="{{ ::doc.joburl }}" class="apply" target="_blank">
<button type="button" class="btn btn-primary btn-xs" data-ng-click="apply(doc.jobid, doc.companyid)"
data-ng-style="docSettings.buttonCss">
<strong>APPLY</strong>
</button>
</a>
</span>
</h4>
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<span class=""><strong>ID: </strong>{{ ::doc.jobid }}</span>
<img data-ng-if="docSettings.heading.logourl && docSettings.heading.logourl != ''"
data-ng-src="{{docSettings.heading.logourl}}" class="side-logo inline-block" id="">
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<strong>Location: </strong><span class="">{{ ::doc.location }}</span>
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<strong>Updated Date: </strong><span class="">{{ ::doc.updateddate }}</span>
</div>
<div class="col-md-12" data-ng-style="docSettings.listCss">
<hr data-ng-style="docSettings.listCss">
</div>
</div>
</div>
There is nothing offensive about the other functions that are called when the button is pressed.
var modalInstance;
$scope.viewJob = function(modalDoc, docSettings) {
$scope.modalDoc = modalDoc;
$scope.docSettings = docSettings;
//the trusAsHtml takes string creates an object, so this will in essence convert string to object
//make sure you check if it is a string since it could be called multiple times by user (close and reopen same modal)
if (modalDoc.overview && typeof modalDoc.overview === 'string') {
$scope.modalDoc.overview = $sce.trustAsHtml(modalDoc.overview);
}
if (modalDoc.qualifications && typeof modalDoc.qualifications === 'string') {
$scope.modalDoc.qualifications = $sce.trustAsHtml(modalDoc.qualifications);
}
if (modalDoc.responsibilities && typeof modalDoc.responsibilities === 'string') {
$scope.modalDoc.responsibilities = $sce.trustAsHtml(modalDoc.responsibilities);
}
modalInstance = $modal.open({
templateUrl: 'app/modal/job_preview.html',
//templateUrl: 'myModalContent.html',
scope: $scope
});
};
I want to optimize this code so it can sever a list of up to 1500, but I cannot for the life of me find the culprit.
I will also take any solutions to reduce the load instead. Like for now I am thinking I may limit the number of DOM elements to 10 to so, and have angular rotate what is being viewed as user scrolls if it will result in better UX.
UPDATE:
Many things have been tried, from use of bind-once to more convoluted solutions that retard some of the watchers Which are enat but require a lot of Math to estimate which items are visible etc.
I finally decided on one solution that was easiest to do: I made a list of only items I wish shown and on mouse scroll up or down I edit the list.
First part of the solution is use of two directives:
.directive('ngMouseWheelUp', function() {
return function($scope, $element, $attrs) {
$element.bind("DOMMouseScroll mousewheel onmousewheel",
function(event) {
// cross-browser wheel delta
var event = window.event || event; // old IE support
var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
if(delta > 0) {
$scope.$apply(function(){
$scope.$eval($attrs.ngMouseWheelUp);
});
// for IE
event.returnValue = false;
// for Chrome and Firefox
if(event.preventDefault) {
event.preventDefault();
}
}
});
};
})
.directive('ngMouseWheelDown', function() {
return function($scope, $element, $attrs) {
$element.bind("DOMMouseScroll mousewheel onmousewheel", function(event) {
// cross-browser wheel delta
var event = window.event || event; // old IE support
var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
//console.log(event);
if(delta < 0) {
$scope.$apply(function(){
$scope.$eval($attrs.ngMouseWheelDown);
});
// for IE
event.returnValue = false;
// for Chrome and Firefox
if(event.preventDefault) {
event.preventDefault();
}
}
});
};
})
These two enable me to disable scrolling in the list on the right side. Then I would create two additional arrays from the documents in routeScope. First list would be generated whenever the documents were updated (which was an event listener for event emitted by the UI of the right hand side graph), this filter would only return array members that had the show property set to true:
var showFilter = function(object) {
return object.show;
}
This would be my array of visible items. From this array I created another Array of shown items. I defined a constant for max size of 7, so at most there are 7 items shown. And of course I set overflow of the parent container to none to disable scrollbar. (I may add a scroll graphic so the user knows he can scroll this field later)
Then I added the following directives to the side div:
data-ng-mouse-wheel-up="listUp()" data-ng-mouse-wheel-down="listDown()"
And inside the controller I defined listUp and listDown to work off an index and the max size constant to figure out which elements from the visible list I should add to the front or the back of the shown list.
/**
* Simulate scrolling up of list by removing bottom element and adding to top
*/
$scope.listUp = function() {
$rootScope.shownDocuments.unshift(getPrev());
$rootScope.shownDocuments.pop();
}
/**
* Simulate scrolling down of list by removing top element and adding to bottom
*/
$scope.listDown = function() {
$rootScope.shownDocuments.push(getNext());
$rootScope.shownDocuments.shift();
}
/**
* return next item in visibleDocuments array
*/
var getNext = function() {
$rootScope.topIndex++;
if ($rootScope.topIndex > $rootScope.visibleDocuments.length) {
$rootScope.topIndex -= $rootScope.visibleDocuments.length;
}
return ($rootScope.visibleDocuments[($rootScope.topIndex+max_shown_size)%$rootScope.visibleDocuments.length]);
}
/**
* Return previous item in visibleDocuments array
*/
var getPrev = function() {
$rootScope.topIndex--;
if ($rootScope.topIndex < 0) {
$rootScope.topIndex += $rootScope.visibleDocuments.length;
}
return ($rootScope.visibleDocuments[$scope.topIndex]);
}
Use of rootScope vs scope is mostly because modals would cause some undesirable behaviors if they were dismissed improperly.
Finally a reset function for the view:
/**
* Resets the list of documents in the visibleList (IE which are visible to client)
*/
var updateVisibleDocuments = function() {
$rootScope.topIndex = 0;
$rootScope.visibleDocuments = $rootScope.documents.filter(showFilter);
//clear view
$rootScope.shownDocuments = [];
$rootScope.topIndex = 0;
for (var i = 0; i < max_shown_size; i++) {
$rootScope.shownDocuments.push(getNext());
}
$rootScope.topIndex = 0;
}
This solution works really well because I only render 7 items even if my list has 100k items. This limits number of watchers tremendously.
You may want to try paginating to reduce the amount of things angular and the browser need to deal with on screen at any one time.

ng-repeat not reordering when change is made to underlying array

I'm using ui.sortable to display a reorderable list of items. I get my data like this:
context.rules.getAll()
.then(
function (data) { // success
$scope.rules = data;
cachedRules = data.slice();
$scope.loaded = true;
},
function (response) { // failure
console.log(response);
});
I use cachedRules so I can compare the array that is being reordered to the original and detect if a change has been made or not. My view looks like this:
<tbody ui-sortable="sortableOptions" ng-model="rules">
<tr ng-repeat="rule in rules|orderBy:'RuleSequence'" ng-class="{'unsortable': !reorder, 'inactive': !rule.Active}">
<td><i ng-show="reorder" class="fa fa-reorder"></i></td>
<td>{{rule.RuleSequence}}</td>
<td>{{rule.ProxyType}}</td>
<td>{{rule.ProxyDesc}}</td>
<td>
<i class="fa fa-download" title="Download CSV" ng-click="getAssignments(rule.RuleID)"></i>
<i class="fa fa-gears" title="Edit Rule" ng-click="editRuleShow(rule)"></i>
</td>
</tr>
</tbody>
Whenever something is reordered, this code gets called so that RuleSequence (what I'm using in my OrderBy) is updated:
$scope.rules.map(function (r) {
r.RuleSequence = $scope.rules.indexOf(r) + 1;
return r;
});
And then a "Save" button becomes enabled if the order of $scope.rules is different from cachedRules. This all works perfectly.
However, I want to have a "Cancel" button that when clicked will revert the display on the page to the original order. Given that I store a copy of the original data this should be easy, I use ng-click to do $scope.rules = cachedRules.slice();, however the order isn't updated on the page after I do that, is stays in it's changed state even though $scope.rules is back to it's unchanged state. How can I get the display to revert back to its original order?
After looking further, it looks like .slice() doesn't do a deep-copy like I thought it did (my in-experience with javascript shining through). So when I did my initial get of the data, and set cachedData, the array itself wasn't a reference but the objects inside of the array still were, so when I updated them here
$scope.rules.map(function (r) {
r.RuleSequence = $scope.rules.indexOf(r) + 1;
return r;
});
It was updating both, so cachedRules would have the updated RuleSequence. When I clicked "Cancel" and set it back to cachedRules, RuleSequence would remain the same. So when I do a proper deep copy there with loDash
context.rules.getAll()
.then(
function (data) { // success
$scope.rules = data;
cachedRules = _.cloneDeep($scope.rules);
$scope.loaded = true;
},
function (response) { // failure
console.log(response);
});
Everything went off without a hitch.

Resources