knockout bind do not updating when observable has changed - javascript

I've a dropdown button which should be avaiable after ajax request will be finished.
<div class="form-input">
<label class="">Sort by:</label>
<select name="orderby" class="selectpicker" data-bind="value: sortBy, optionsCaption: 'Default', disable: waiting">
<option value="some_value">some_option</option>
<option value="some_value">some_option</option>
</select>
</div>
On page requested, it initially load data
$(function() {
//Initialization
var vm = new ArticleViewModel();
initialLoadArticles(vm);
ko.applyBindings(vm, $("#article-plugin")[0]);
});
function ArticleViewModel() {
var self = this;
//options =>
this.articles = ko.observableArray([]);
this.pageSize = 12;
this.sortBy = ko.observable('asc');
this.currentPage = ko.observable(1);
this.waiting = ko.observable(true);
this.totalPages = 0;
this.initMode = true;
this.timerId = null;
this.viewTemplate = ko.observable('listview-template');
if (this.viewTemplate() === "listview-template") {
this.pageSize = 4
} else {
this.pageSize = 12
};
this.sortBy.subscribe(function(event) {
console.log(event);
self.optionChanged();
loadArticles(self);
});
this.optionChanged = function() {
this.currentPage(1);
}
this.setCardView = function() {
self.viewTemplate('cardview-template');
loadArticles(self);
}
this.setListView = function() {
self.viewTemplate('listview-template');
loadArticles(self);
}
}
function initialLoadArticles(vm) {
vm.waiting(true);
var params = {
page: vm.currentPage(),
size: vm.pageSize,
sortby: vm.sortBy()
};
api.ajax.get(api.urls.article.getArticles, params, function(response) {
console.log('waiting: ' + vm.waiting());
if (response.success) {
vm.articles(response.data.items);
vm.waiting(false);
}
});
}
Well, on a page it display all articles, but dropdown button still blocked and I don't what exactly could be the problem of that.

I'd suggest a few changes to your viewmodel, featuring automatic loading via a subscription.
I think you always want to set waiting to false after loading, independent of whether the request was a success or not. Also think about low-level request errors, you need to add a handler for those.
function ArticleViewModel() {
var self = this;
self.articles = ko.observableArray();
self.pageSize = ko.observable();
self.sortBy = ko.observable('asc');
self.currentPage = ko.observable();
self.waiting = ko.observable(true);
self.viewTemplate = ko.observable();
// API
self.setCardView = function() {
self.viewTemplate('cardview-template');
self.pageSize(12);
self.currentPage(1);
};
self.setListView = function() {
self.viewTemplate('listview-template');
self.pageSize(4);
self.currentPage(1);
};
// compute Ajax-relevant parameters
self.ajaxParams = ko.pureComputed(function () {
return {
page: self.currentPage(),
size: self.pageSize(),
sortby: self.sortBy()
};
}).extend({ rateLimit: { timeout: 10, method: 'notifyWhenChangesStop' } });
// auto-load when params change
self.ajaxParams.subscribe(function (params) {
self.waiting(true);
api.ajax.get(api.urls.article.getArticles, params, function (response) {
if (response.success) {
self.articles(response.data.items);
}
self.waiting(false);
});
});
// set inital view (also triggers load)
self.setListView();
}
$(function() {
var vm = new ArticleViewModel();
ko.applyBindings(vm, $('#article-plugin')[0]);
});
More strictly speaking, you I'd advice against true or false as the "loading" indicator. It's technically possible that more than one Ajax request is running and this would be a race condition. The first request that comes back resets the "loading" state, and the next one still overwrites the viewmodel data. Either use a counter, or prevent new requests while there is a pending one.
The rateLimit extender makes sure that a rapid succession of changes to the parameters, like what happens when setListView() is called, does not cause multiple Ajax requests.
If your Ajax requests are done by jQuery internally, I would suggest the following setup to be able to make use of the done, fail and always promise handlers:
function ApiWrapper() {
var self = this;
function unwrapApiResponse(response) {
if (response.success) {
return new $.Deferred().resolve(response.data).promise();
} else {
return new $.Deferred().reject(response.error).promise();
}
}
self.getArticles = function (params) {
return $.get('articleUrl', params).then(unwrapApiResponse);
};
// more functions like this
}
var api = new ApiWrapper();
and in your viewmodel:
self.ajaxParams.subscribe(function (params) {
self.waiting(true);
api.getArticles(params).done(function (data) {
self.articles(data.items);
}).fail(function (err) {
// show error
}).always(function () {
self.waiting(false);
});
});

Related

'No Data' view getting opens first and then Detail Page opens with the data in Fiori

I am developing a Master Detail application in which if the service URL doesn't return data, then a view called 'NoData' should open. But what actually is happening that first, the 'NoData' view opens and then the Detail Page with the data gets displayed. I don't know why and how that 'NoData' page is appearing first. Below is my code for Master Page :
Controller.js :
onInit: function () {
this.router = sap.ui.core.UIComponent.getRouterFor(this);
this._custTemp = this.getView().byId("listItemTemp").clone();
this.refreshFlag = true; // Flag to get new data or not for customers
this.totalModel = sap.ui.getCore().getModel("totalModel");
this.getView().setModel(this.totalModel, "totalModel");
this.oDataModel = sap.ui.getCore().getModel("DataModel");
this.getView().setModel(this.oDataModel, "DataModel");
this.oInitialLoadFinishedDeferred = jQuery.Deferred();
var oEventBus = sap.ui.getCore().getEventBus();
this.getView().byId("listId").attachEvent("updateFinished", function () {
this.oInitialLoadFinishedDeferred.resolve();
oEventBus.publish("MasterPage", "InitialLoadFinished", {
oListItem: this.getView().byId("listId").getItems()[0]
});
if (!sap.ui.Device.system.phone) {
this._getFirstItem();
}
}, this);
this.functionData = [];
},
waitForInitialListLoading: function (fnToExecute) {
jQuery.when(this.oInitialLoadFinishedDeferred).then(jQuery.proxy(fnToExecute, this));
},
_getFirstItem: function () {
sap.ui.core.BusyIndicator.show();
this.waitForInitialListLoading(function () {
// On the empty hash select the first item
var list = this.getView().byId("listId");
var selectedItem = list.getItems()[0];
if (selectedItem) {
list.setSelectedItem(selectedItem, true);
var data = list.getBinding("items").getContexts()[0];
sap.ui.getCore().getModel("detailModel").setData(data.getObject());
this.router.navTo('DetailPage', {
QueryNo: data.EICNO
});
sap.ui.core.BusyIndicator.hide();
} else {
this.router.navTo('NoData');
}
}, this);
},
onBeforeRendering: function () {
this._fnGetData();
},
_fnGetData: function (oEvent) {
var that = this;
this.getView().setModel(this.totalModel, "totalModel");
if (this.refreshFlag === true) {
sap.ui.core.BusyIndicator.show(0);
$.ajax({
url: "/sap/opu/odata/sap/ZHR_V_CARE_SRV/EmpQueryInitSet('10002001')?$expand=QueryLoginToQueryList/QueryToLog",
method: "GET",
dataType: "json",
success: function (data) {
that.getView().getModel("totalModel").setData(data.d.QueryLoginToQueryList);
that.refreshFlag = false;
sap.ui.core.BusyIndicator.hide();
that.statusList();
},
error: function (err) {
sap.ui.core.BusyIndicator.hide();
MessageBox.information(err.responseText + "Please try again");
}
});
}
}
totalModel is a json model, right? You'll get two updateFinished events on app load. The first one is triggered once the list control is rendered and binding is done (when the model has no data), and the second comes after your $.ajax call updates data to totalModel.
I think you can solve it by moving your NoData navigation to both 'success' and 'error' callbacks of your $.ajax call. Doing so may cover other use cases e.g. if you are using URL navigation parameters and a user changes the entity ID in the URL to some random number, it'd navigate to your NoDatapage.

AngularJs+Bootstrap+Typehead+Ajax is working only if i put alert box but only in chrome

i am using bootsrap typehead with angularjs given at this link http://angular-ui.github.io/bootstrap/
In my controller
$scope.getUser = function(val) {
//alert("hi");
return $http.get('user/getUserNames.do', {
params: {
userName: val,
}
}).then(function(response){
return response.data;
});
};
my html code
<input type="text" ng-model="asyncSelected" typeahead-wait-ms="300" typeahead="user for user in getUser($viewValue)" class="form-control">
if remove the alert the typehead will not work
if i keep the alert the typehead will work only in chrome
if i place a break point at "return $http.get('user/getUserNames.do'" and step out using
fire bug it works in firefox
i am receiving the data in this formate ['name1','name2'] from server
some one please help
thanks in advance
thats because the time taken to close the alert the async data is recieved. you should store the data on $scope rather then calling a function on $scope
$scope.users= {};
$scope.getUser = function(val) {
return $http.get('user/getUserNames.do', {
params: {
userName: val,
}
}).then(function(response){
$scope.users= response.data;
});
};
html
<input type="text" ng-model="asyncSelected" ng-change="getUser($viewValue)"
typeahead-wait-ms="300" typeahead="user for user in users" class="form-control">
your cods logic is incorrect,you cant return data like that from a async function, that need time to complete,
dont return anything from this getUser function. you have 2 option :
1 - store the responce.data in a global variable to be used later
$scope.users = [];
$scope.getUser = function (val) {
$http.get('user/getUserNames.do', {
params: {
userName: val
}
}).then(function (response) {
$scope.users.push(response.data);
});
};
2 - call another function when get function is complete to handle the data recived
$scope.getUser = function (val) {
$http.get('user/getUserNames.do', {
params: {
userName: val
}
}).then(function (response) {
$scope.userLoaded(response.data);
});
};
By the simple hack in angular-ui-bootstrap i solved the problem
before..........
var getMatchesAsync = function(inputValue) {
var locals = {$viewValue: inputValue};
isLoadingSetter(originalScope, true);
$q.when(parserResult.source(originalScope, locals)).then(function(matches) {
//it might happen that several async queries were in progress if a user were typing fast
//but we are interested only in responses that correspond to the current view value
var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
if (onCurrentRequest && hasFocus) {
if (matches.length > 0) {
scope.activeIdx = focusFirst ? 0 : -1;
scope.matches.length = 0;
//transform labels
for(var i=0; i<matches.length; i++) {
locals[parserResult.itemName] = matches[i];
scope.matches.push({
id: getMatchId(i),
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
//due to other elements being rendered
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
element.attr('aria-expanded', true);
} else {
resetMatches();
}
}
if (onCurrentRequest) {
isLoadingSetter(originalScope, false);
}
}, function(){
resetMatches();
isLoadingSetter(originalScope, false);
});
};
i just removed '&& hasFocus' this sipneet from the code
after ........
var getMatchesAsync = function(inputValue) {
var locals = {$viewValue: inputValue};
isLoadingSetter(originalScope, true);
$q.when(parserResult.source(originalScope, locals)).then(function(matches) {
//it might happen that several async queries were in progress if a user were typing fast
//but we are interested only in responses that correspond to the current view value
var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
if (onCurrentRequest) {
if (matches.length > 0) {
scope.activeIdx = focusFirst ? 0 : -1;
scope.matches.length = 0;
//transform labels
for(var i=0; i<matches.length; i++) {
locals[parserResult.itemName] = matches[i];
scope.matches.push({
id: getMatchId(i),
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
//due to other elements being rendered
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
element.attr('aria-expanded', true);
} else {
resetMatches();
}
}
if (onCurrentRequest) {
isLoadingSetter(originalScope, false);
}
}, function(){
resetMatches();
isLoadingSetter(originalScope, false);
});
};

How to cancel asynchronous process in javascript?

I have a one-window javascript application. I have a dashboard that displays certain images by loading via multiple get requests in the background.
Problem arises when not all get requests are finished on time and the context of the site changes because then I want to clear the dashboard. Yet if the get request havent't finished yet, they will populate the dashboard with the wrong images.
I am trying to think of a way to abort those get request. Can someone please direct me in the right direction?
var Dashboard = {
showAllAssets: function(){
var self = this;
this.resetDashboard();
$.get(this.urlForAllAssets, function(json){
self.loadAssets(json);
});
},
showAssetsForCategory: function(categoryId) {
...
},
getHtmlForAsset: function(id) {
var self = this;
$.get(this.urlForDashboardThumb + "/" + id.toString(), function(assetHtml){
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
// this gets inserted even when context changed, how can I prevent that?
var thumb = Object.create(Thumbnail);
thumb.init($asset);
}, 'html')
},
insertAssetThumbIntoDom: function($asset) {
$asset.appendTo(this.$el);
},
resetDashboard: function() {
this.$el.html("");
},
loadAssets: function(idList) {
var self = this;
var time = 200;
// These get requests will pile up in the background
$.each(idList, function(){
var asset = this;
setTimeout(function(){
self.getHtmlForAsset(asset.id);
}, time);
time += 200;
});
},
bind: function() {
$document.on('loadAssets', function(event, idList) {
self.loadAssets(idList);
});
$document.on('switched_to_category', function(event, categoryId) {
self.showAssetsForCategory(categoryId);
});
$document.on('show_all_assets', function(){
self.showAllAssets();
})
},
init: function($el) {
this.$el = $el;
this.resetDashboard();
this.bind();
}
}
Though you cant stop an already sent request, you can still solve your problem.
My solution is to generate a simple ID, a random set of numbers for example, and store somewhere in your dashboard, and send it along with the request and send it back with the image.
If a new context is generated, it will have a new ID.
If the image comes back with a different ID than the one in the current context, then discard it.
As pointed out by the comments, a possible solution is to store the current context and compare it within the success method on the get request.
I have changed my code insofar that now I'll store the current within the manager and also I pass the event around to the $.get-method.
This has the downside that the get requests are still processed though and the loading of the new context takes longer as those get requests are processed later if there are too many to process. I also dislike passing the event around.
var Dashboard = {
currentLoadEvent: null,
loadAssets: function(idList, event) {
var self = this;
$.each(idList, function(){
var asset = this;
self.getHtmlForAsset(asset.id, event);
});
},
getHtmlForAsset: function(id, event) {
var self = this;
$.get(this.urlForDashboardThumb + "/" + id.toString(), function(assetHtml){
if (event === self.currentLoadEvent) {
console.log('same event continuing');
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
var thumb = Object.create(Thumbnail);
thumb.init($asset);
} else {
console.log('context changed');
}
}, 'html')
},
bind: function() {
var self = this;
$document.on('loadAssets', function(event, idList) {
self.currentLoadEvent = event;
self.loadAssets(idList, event);
});
$document.on('switched_to_category', function(event, categoryId) {
self.currentLoadEvent = event;
self.showAssetsForCategory(categoryId, event);
});
$document.on('show_all_assets', function(event){
self.currentLoadEvent = event;
self.showAllAssets(event);
})
}
}
I created a different solution by storing the request in an array and aborting them when the context changed:
loadAssets: function(idList, event) {
var self = this;
var requests = [];
$.each(idList, function(){
var asset = this;
if (self.currentLoadEvent === event){
var request = $.get(self.urlForDashboardThumb + "/" + asset.id.toString(), function(assetHtml){
if (event === self.currentLoadEvent) {
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
var thumb = Object.create(Thumbnail);
thumb.init($asset);
console.log('completed get request');
} else {
console.log('context changed');
$.each(requests, function(){
this.abort();
console.log('aborted request');
})
}
}, 'html');
requests.push(request);
} else {
return false;
}
});
},

js oop style working in chrome but not in firefox

I have this script and it runs fine in chrome (and in JS online validators) but firefox throws me this error:
screen.initialize is not a function
Is this syntax somehow not in accordance with standards?
$(document).ready(function() {
screen = new Screen('t543f3r','user1','screen4',5);
screen.initialize();
}
Here is the Screen class:
//Our screen object
function Screen(hashKey,username,screenName,layout) {
this.hashKey = hashKey;
this.username = username;
this.screenName = screenName;
this.layout = layout;
//this.cacheRefreshInterval = 1000*60*5;
this.checkLayoutInterval = 1000*60*5; //check for new cache every 5 minutes
}
Screen.prototype.initialize = function() {
var self = this;
console.log('initializing screen '+this.screenName+' (layout is ['+this.layout+']) on player '+this.username+' using key '+this.hashKey);
var time = self.checkLayoutInterval;
setTimeout(function(){self.getValidLayout();}, time);
console.log('getValidLayout() set for '+time);
}
Screen.prototype.getValidLayout = function() {
var self = this;
var url = self.findBaseUrl(true) + 'getValidLayout/'+self.hashKey;
jQuery.ajax({
async: true,
url: url,
success: function(result) {
console.log('successfully fetched the valid screen layout: ['+result+']');
if (result != self.layout) {
window.location.reload();
}
},
error: function(result) {
console.log('there was an error fetching the screen layout');
},
complete: function() {
//setup next check
var time = self.checkLayoutInterval;
setTimeout(function(){self.getValidLayout();}, time);
console.log('next getValidLayout() set for '+time);
}
});
}
window.screen is a readonly property. When you assign to it (because you didn't declare your screen as a var), the assignment gets ignored.

knockoutjs data saving UI issue

In my web app while I am updating some data I need to show some loading spinning gif in the web page.
This is my code.
This is my html code
<img src="../../../../Content/images/submit-gif.gif" class="hidegif" data-bind="visible: isWaiting"/>
<button data-bind="click: createNew">Save</button>
In my knockoutjs model I have this
self.isWaiting = ko.observable(false);
self.createNew = function () {
this.isWaiting(true);
$.getJSON("/Admin/Material/GetFolders", function (allData) {
this.isWaiting(true);
var mappedFolders = $.map(allData, function (item) { return new Folder(item); });
self.folders(mappedFolders);
this.isWaiting(false);
}).success(function () { this.isWaiting(false); }).error(function () { }).complete(function () { this.isWaiting(false); }); ;
};
I have property called isWaiting. Before I call the server I am setting it to true. In completion and successive method I am setting it back to false.
So based on that my spinning wheel should appear and disappear.
But this is not working.
Thanks In Advance
this will have another context inside the createNew and callback functions. You should use self instead of this for accessing view model's property:
self.createNew = function () {
self.isWaiting(true);
$.getJSON("/Admin/Material/GetFolders", function (allData) {
self.isWaiting(true);
var mappedFolders = $.map(allData, function (item) { return new Folder(item); });
self.folders(mappedFolders);
self.isWaiting(false);
}).success(function () {
self.isWaiting(false);
}).error(function () {})
.complete(function () {
self.isWaiting(false);
});
};

Categories

Resources