I really, really don't get it anymore. I'm trying to develop an interface using Knockout, but trying to 'toJSON' it gives me headaches.
The problem is as follows:
I have a FormViewModel. This form contains logic and meta data (observables) about the form itself.
I have FormSection, which only has 2 observables.
I have Deletion, also just 2 observables.
I wanna AJAX that to a PHP file receiving the JSON object, but I've got no clue how to proceed from here. I just want a number of observables, not all of them. I also wanna be able to load an initial state FROM an Ajax call (with JSON).
Here are the relevant parts of my code:
// This only has some observables
var FormSection = function(number)
{
var self = this;
this.title = ko.observable('Section ' + number);
this.selectedPageStyle = ko.observable();
};
// Only observable / computed
var Deletion = function(page)
{
var self = this;
// reference to a just deleted page, to make 'undeletion' possible
this.page = page;
this.pageTitle = ko.computed(function(){ return self.page.title() });
};
So far so good, but I've got my ViewModel with some observables, some functionality, etc. This is what I've got so far: (I removed the body of functions for readability)
var FormViewModel = function(data)
{
var self = this;
this.pages = ko.observableArray([]);
this.deletions = ko.observableArray([]);
this.name = ko.observable();
this.description = ko.observable();
this.availablePageStyles = ko.observableArray(['Header', 'Fields', 'Signatures']);
this.hasPages = ko.computed(function(){}, this);
this.numberOfPages = ko.computed(function(){}, this);
self.selectedPage = ko.observable( );
self.isPageSelected = function(page) {};
self.clearPage = function(data, event) {};
this.save = function() {
$.ajax("x", {
// WHAT TO DO?!
data: { data: ko.toJSON(this)},
type: "post",
//success: function(result) { alert(result) },
error : function(jqXHR, textStatus, errorThrown) { alert(textStatus + errorThrown)}
});
};
$.getJSON("x", function(allData) {
// How to populate back?!
});
this.addPage = function(data, event){}
this.removePage = function(page){}
this.unremovePage = function(deletion){}
};
I've got no idea how to proceed if I wanna save the relevant observables. For example: I don't need self.selectedPage. I use that for sorting only.
Do I use mapping here?
How to map the relevant observables?
How do I put that in JSON?
How do I 'map back' from the JSON I receive from the server?
To control what is converted to JSON when using ko.toJSON you can overload the function by doing
viewModel.prototype.toJSON = function(){
var copy = this;
delete copy.//whatever variables you don't want to return to server here
//any other variables you want to not return
return copy;
}
I put together a sample fiddle http://jsfiddle.net/Rynan/hA4Kz/.
For more information and samples see http://www.knockmeout.net/2011/04/controlling-how-object-is-converted-to.html
Related
I'm trying to call an API that takes parameters start and count, like this:
function handleSuccess() {
if (!!response.data) {
return (response.data);
} else {
return q.reject(response.data);
}
}
function handleError() {
// do some handling
}
function getData(url, sortBy) {
var count = 10;
var start = 1;
var request = http({
cache: true,
method: "GET",
url: url,
params: {
sortBy: sortBy,
sortOrder: "ASC",
count: count, // e.g. 10
start: start // e.g. 1
}
});
return (request.then(handleSuccess, handleError));
}
The JSON response from the API could contain a "next" link that would give me the URL to call to get the next set of data if there exists more...this is how the pagination works.
What's the best way to do this and concatenate all the data returned into one JSON response?
Assuming some part of the data response is an array, then simply use normal array concat() to combine it with previous pages of data in your handleSuccess() callback.
I've found that a service-oriented way is most useful when trying to get paged data from the same endpoint as it is easy to share services and objects between controllers and directives.
First, I would set up the service layer of your application so that all objects being requested have common, generic methods like so (I highly recommend you use ngResource or RESTAngular or something like that):
angular.module('myModule')
.factory('ApiObject', function($http, $q) {
ApiObject = function ApiObject(attributes) {
angular.extend(this, attributes);
};
ApiObject.query = function(url, parameters) {
var deferred = $q.defer();
$http.get(url, {params: parameters}).then(function(data) {
var results = [];
angular.forEach(data, function(apiObject) {
results.push(new ApiObject(apiObject));
});
deferred.resolve(results);
}, function(error) {
// Do error stuff
deferred.reject(error);
});
return deferred.promise;
};
return ApiObject;
});
Then set up a service to manage your paging data that accepts your generic services as well as parameters and configuration options. Also allow for events to be triggered within the service (see trigger and on methods) so that you know when new results are fetched. I've also written in a way for the results to be automatically concatenated onto the current result set:
angular.module('myModule')
.factory('SearchService', function() {
SearchService = function SearchService(service, params, config) {
this.searchParams = params || {};
this.config = config || {};
this.service = service;
this.results = [];
this.listeners = {};
};
SearchService.prototype.fetch = function(params) {
var _this = this;
this.service.query().then(function(results) {
if(_this.config.concatResults) {
_this.results = _this.results.concat(results);
// You probably should make sure results are unique at this point as that is a common problem with paging a changing API
} else {
_this.results = results;
}
_this.trigger('searchSuccess', _this.results);
});
};
SearchService.prototype.on = function(event, listener) {
(this.listeners[event] = (this.listeners[event] || [])).push(listener);
};
SearchService.prototype.trigger = function(event, payload) {
angular.forEach(this.listeners[event], function(listener) {
listener(payload);
});
};
SearchService.prototype.isLastPage = function() {
//logic here to determine last page
};
SearchService.prototype.nextPage = function() {
if(this.isLastPage()) {
return;
}
if(this.searchParams.page) {
this.searchParams.page++;
} else {
this.searchParams.page = 2;
}
this.fetch();
};
// Write more methods for previousPage, lastPage, firstPage, goToPage... etc.
return SearchService;
});
Then in your controller, you're going to want to instantiate the search service with some default parameters and configuration and then fetch the first page:
angular.module('myModule')
.controller('MyCtrl', function($scope, ApiObject, SearchService) {
$scope.searchService = new SearchService(ApiObject, {page: 1}, {concatResults: true});
$scope.searchService.on('searchSuccess', function(results) {
// Do something with results if you wish, but they'll already be stored in $scope.searchService
});
// Get the first page of data
$scope.searchService.fetch();
});
This is obviously a rough cut with a lot of room for improvement, but I hope this will be a good jumping off point to get you pointed in some sort of angular-style direction. In my experience, this is the best way to abstract out paging logic from the data/request layer in your services.
I'm attempting to implement an asynchronous computed observable as show here.
I can do it successfully for one ajax call. The challenge I have at the moment is how to perform various ajax calls in a loop building an array asynchronously and then returning the array to my computed observable array using jQuery promises.
Basically the HTML form works in the following way:
This a student course form.
For each row, users type the person number and on another column they'll type a list of course ids separated by commas. Eg 100, 200, 300.
The purpose of the computed observable is to store an array
containing course details for the courses entered in step 2.
The details are obtained by firing ajax calls for each course and storing HTTP response in the array.
I don't want users to wait for the result, thus the reason to implement an async computed observable.
My problem: I'm having problem returning the value of the final array to the observable. It's always undefined. The ajax calls work fine but perhaps I'm still not handling the promises correctly.
Here's the code for my class:
function asyncComputed(evaluator, owner) {
var result = ko.observable(), currentDeferred;
result.inProgress = ko.observable(false); // Track whether we're waiting for a result
ko.computed(function () {
// Abort any in-flight evaluation to ensure we only notify with the latest value
if (currentDeferred) { currentDeferred.reject(); }
var evaluatorResult = evaluator.call(owner);
// Cope with both asynchronous and synchronous values
if (evaluatorResult && (typeof evaluatorResult.done == "function")) { // Async
result.inProgress(true);
currentDeferred = $.Deferred().done(function (data) {
result.inProgress(false);
result(data);
});
evaluatorResult.done(currentDeferred.resolve);
} else // Sync
result(evaluatorResult);
});
return result;
}
function personDetails(id, personNumber, courseIds) {
var self = this;
self.id = ko.observable(id);
self.personNumber = ko.observable(personNumber);
self.courseIds = ko.observable(courseIds);
// Computed property to extract PIC details for additional PICs.
// This is computed observable which returns response asynchronously
self.courseDetails = asyncComputed(function () {
var courseIdsArray = self.courseIds().split(",");
var arr = [];
var arr_promises = [];
function getCourseDetails(courseId) {
var dfrd = $.Deferred();
var content = {};
content.searchString = courseId;
var url = 'MyURL';
return $.ajax(url, {
type: 'POST',
dataType: 'json',
data: requestData, // content of requestData is irrelevant. The ajax call works fine.
processdata: true,
cache: false,
async: true,
contentType: "application/json"
}).done(function (data) {
arr.push(new PicDetails(data.GenericIdentifierSearchResult[0]));
}).fail(function () {
alert("Could not retrieve PIC details");
}).then(function () {
dfrd.resolve();
});
}
if (courseIdsArray.length > 0) {
$.each(courseIdsArray, function (index, courseId) {
if (courseId.length > 0) {
arr_promises.push(getCourseDetails(courseId));
}
});
};
$.when.apply($, arr_promises).done(function () {
return arr;
})
}, this);
}
I think you dont really need a separate api/code for this.
You could just create observables for every input/value that changes on your site, and create a computed observable based on those.
e.g in rough pseudo code
self.id = ko.observable(id);
self.personNumber = ko.observable(personNumber);
self.courseIds = ko.observable(courseIds);
self.courseDetailsArray = ko.observableArray([]);
self.courseDetails = ko.computed(function() {
//computed the course details based on other observables
//whenever user types in more course ids, start loading them
$.get( yoururl, {self.courseIds and self.id}).success(data) {
when finished async loading, parse the data and push the new course details into final array
self.courseDetailsArray.push( your loaded and parsed data );
//since courseDetailsArray is observableArray, you can have further computed observables using and re-formatting it.
}
});
I have something a bit different from your approach, but you can build something like an asyncComputed out of it if you prefer:
make a simple observable that will hold the result
make a dictionary of promises that you'll basically keep in sync with the array of course ids
when the array of course ids change, add / remove from the dictionary of promises
wrap all your promises in a when (like you're doing) and set the result when they're all done
Basic idea:
var results = ko.observable([]);
var loadingPromises = {};
var watcher = ko.computed(function () {
var ids = ko.unwrap(listOfIds);
if (ids && ids.length) {
ids.forEach(function (id) {
if (!loadingPromises.hasOwnProperty(id)) {
loadingPromises[id] = $.get(url, {...id...});
}
});
var stillApplicablePromises = {};
var promises = []; // we could delete from loadingPromises but v8 optimizes delete poorly
Object.getOwnPropertyNames(loadingPromises).forEach(function (id) {
if (ids.indexOf(id) >= 0) {
stillApplicablePromises[id] = loadingPromises[id];
promises.push(loadingPromises[id]);
}
});
loadingPromises = stillApplicablePromises;
$.when.apply(this, promises).then(function () {
// process arguments here however you like, they're the responses to your promises
results(arguments);
});
} else {
loadingPromises = {};
results([]);
}
}, this);
This is the file (that may change) where you can see this "in real life": https://github.com/wikimedia/analytics-dashiki/blob/master/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
And the basic fiddle: http://jsfiddle.net/xtsekb20/1/
I'm using jQWidgets UI framework with Knockout.js and datajs library(for supporting OData) for building client side of my app. And OData Endpoint in ASP.NET Web API2 at the server side. I created ViewModel for jqWidgets Grid as in code below:
var vm = function() {
this.items = ko.observableArray();
var self = this;
//qet data from service
OData.read(url,
function success(data, response) {
//convert data to observable array
self.items(data.results);
},
function error(err) {
alert(err.message);
});
this.removeItem = function() {
// remove item
var element = "#jqxgrid";
var cell = $(element).jqxGrid('getselectedcell');
if (cell != null && cell != "") {
var selectedrowindex = cell.rowindex;
};
var item =$(element).jqxGrid('getrowdata', selectedrowindex);
OData.request({
requestUri: url + '(' + item.CompanyID + ')',
method: "DELETE",
},
function success(data, response) {
alert('DELETE successfull');
},
function error(err) {
alert(err.message);
});
};
As you see I can get and remove items.
My problem is how to save ALL changes and send JUST changed items to server. For add/update entities at server side I have to send POST/PUT request with appropriate json object (not collection of objects). So, for example, if I want to update all changed items, i have to do PUT request for each item.
Is it any way to detect which items in observableArray was added/changed and send each of those items to server??
knockout does not do this out of the box. What I have done in the past is set an isDirty flag in the object stored in the array (this assumes your object is populated with observables, if not this wont work, and there is no easy way to do it using regular js objects). The is dirty flag watches the observable properties for changes and when on is made it sets the flag to true. Then when saves are being made you just see if the record for isDirty() == true
var entryForm = function(){
var self = this;
self.firstName = ko.observable();
self.lastName = ko.observable();
self.email = ko.observable();
self.dirty = ko.observable(false);
self.dirtyCalculations = ko.computed(function(){
//read from any observable we want to watch as part of our "dirty" calculation
self.firstName();
self.lastName();
self.email();
//if any of the above changed, this method will be called, so this model is now "dirty"
self.dirty(true);
});
//see the next section for an explanation of these lines
self.resetDirtyFlag = function(){self.dirty(false);}
self.resetDirtyFlag();
}
I see in your code above that you plug your return object straight into the array without converting the properties to observable. I would suggest converting the properties and using a similar method above.
So my Backbone.js code is getting the JSON... I am trying to simply console out the models in the success callback of the fetch method but I just get back [r,r,r] instead of [object,object,object]. Pulling my hair out...
var Person = Backbone.Model.extend();
var PersonCollection = Backbone.Collection.extend({
model : Person,
url: 'js/names.json',
parse: function(data) {
console.log(data); // <--- this will return what I am looking for
return data;
}
});
var PersonView = Backbone.View.extend({
initialize: function(){
var self = this;
self.collection = new PersonCollection();
self.collection.fetch({success: function() {
console.log(self.collection.models); // <-- how do I get it here?
}});
}
});
var newView = new PersonView();
JSON
[
{ "name": "Linda", "birthYear": 1947},
{ "name": "Kat", "birthYear": 1977},
{ "name": "Jen", "birthYear": 1989}
]
edit: I want the same thing after the fetch I get when I console.log the data in a custom parse method in the collection. See comments in code above
You are getting confused. The models in the collection are Backbone models which wrap around your records, not anything directly resembling your JSON you are giving to the collection. If you want that, consider console.log(JSON.stringify(self.collection)).
Looking here suggests that your callback function will get passed the items, e.g.
users.fetch({
success: function (users) {
var template = _.template($('#user-list-template').html(), {users: users.models});
that.$el.html(template);
}
})
So maybe a tweak to your callback will help...
I tried this code, adding to toJSON now log the same object that parse function logged. If you are not getting same output there could be some thing wrong with other part of the code.
var Person = Backbone.Model.extend();
var PersonCollection = Backbone.Collection.extend({
model : Person,
url: 'js/names.json',
parse: function(data) {
console.log(data); // <--- this return array of name objects
return data;
}
});
var PersonView = Backbone.View.extend({
initialize: function(){
var self = this;
self.collection = new PersonCollection();
self.collection.fetch({success: function() {
console.log(self.collection.toJSON()); // <-- even this return array of name objects
}});
}
});
var newView = new PersonView();
The following is the code sample I have written. I would like to get the data from REST services and print it on the screen. I could get the response from REST in the JSON format. But, I could not find the way to use store it in the JSONStore and use it. Please help me to resolve this issue.
dojo.provide("index.IndexService");
dojo.require("dojo.parser");
dojo.require("dijit.Editor");
dojo.require("dijit.form.Button");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.TabContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dojox.data.JsonRestStore");
var xhrArgs = {
url: "http://localhost:8080/RestApp/service/customers",
handleAs: "json",
contentType : "application/json",
load: function(data){
alert(data);
},
error: function(error){
alert(error);
}
}
dojo.ready(function(){
// Create a button programmatically:
var button = new dijit.form.Button({
label: "View Transactions...",
onClick: function(){
// Do something:
dojo.byId("result1").innerHTML += "Functionality yet to be implemented! ";
}
}, "progButtonNode");
alert('hi');
var store = new dojox.data.JsonRestStore({target: "http://localhost:8080/RestApp/service/customers"});
alert(store);
store.get(1).when(function(object){
alert(object);
// use the object with the identity of 3
});
//var deferred = dojo.xhrGet(xhrArgs);
//compliantStore = new dojox.data.JsonRestStore({deferred});
alert(deferred);
});
Returned JSON value is
{"employees":{"customers":[{"city":null,"accountNo":null,"name":"Name
1","id":1},{"city":null,"accountNo":null,"name":"Name
2","id":2}],"count":2}}
How would I retrive the values?
JsonRestStore items are actually simple JavaScript objects, therefore you can always directly read properties from items. Like
var store = new dojox.data.JsonRestStore({target: "http://localhost:8080/RestApp/service/customers"});
myValue = recipeStore.getValue(item,"foo"); //stored in myValue
get = store.getValue;
set = store.setValue;
save = store.save;
// then fetch an item
var myGetValue = get(item,"foo");
var setMyValue = set(item,"foo","bar");
In synchronous mode, one can fetch without providing a callback, by directly accessing the results property from the request object that is returned from the fetch operation:
var queryResults = store.fetch({query:"?tastes='good'"}).results;
var firstItem = queryResults[0];
Did you meant something like that.. Hope it helps