Clone sapUi5 json model and stop binding - javascript

I have seen a similar question, but in my case it doesn't work.
I have a JSON model, called data, which corresponds to a SAPUi5 form with comboboxes. I want to copy the state of the model the first time I open my application and keep it like that. After that I want to use it to reset my form and bring the comboboxes back to their default values.
When I first start my application:
this.getView().setModel(new JSONModel(data)); //create the original model
//copy the original model (copyModel is global variable
copyModel = $.extend({}, data);
Until here everything is fine. The two models are exactly the same. After that I have a button and a reset Function:
resetP: function(){
this.getView().setModel(new JSONModel(copyModel));
console.log(copyModel);
}
The first time I select something in the comboboxes and click the reset button and run the reset function, the copymodel is the right one. Same with the original data model. When I change again the selected value of the combobx, the copyModel, starts taking the selected value. Somehow it's overwritten. I don't know what I am doing wrong. Are there any suggestions? I have also tried to use JSON.strignify instead of extend.

JSON models be default have two way binding. So when you are triggering events like selectionChange on the ComboBox, because of two way binding, the set data to the model keeps getting updated. Also Javascript has objects by reference, so it is the original copyModel object that gets updated.
You can prevent this by setting a copy of the copyModel to the JSON model.
Another thing I would like to mention is that do not keep setting the model again and again.
You can just update the data that is set to the model and update the model.
This can be done in 2 ways.
a.
resetP: function(){
this.getView().getModel().setData(copyModel);
console.log(copyModel);
}
b. You could also update the required property and do a
this.getView().getModel().updateBindings();

We use jQuery.extend(true, {}, object_to_copy); in this way to create a "deep copy" from the object we want an independed copy from.

Related

Where to store initial Object's values for the Objects that can be changed, in AngularJS?

I'm using a factory to create different user-Objects from data which comes from the Server. Each user-Object has a "userGroup"-property. A list of users is displayed using ng-repeat in the View. And there it is possible to change the userGroup-value from "basic" to "admin", because of the AngularJS 1 two way binding. So the original values of the Object are gone. So my question is: when we want to cancel the made changes where should the initial value {userGroup: "basic"} be stored?
I was thinking about two possible solutions:
create an additional property "initUserGroup" in the User Factory Class and store a value for each Object
use localstorage (up to 20 records must be saved at once)
Are there any best practices for such cases?
For example, you can backup whole object in a property like _backup (using angular.copy) and restore if you with to undo changes (using angular.extend). Here is an example:
$scope.editItem(item) {
item._backup = angular.copy(item);
}
$scope.undoEdit(item) {
angular.extend(item, item._backup);
//delete unused data
delete(item._backup);
}
In this case you won't need to save data outside current object.

KnockoutJS: Stop a particular property from setting dirty flag

With some help from StackOverflow community I was able to get my dirty flag implementation to work, based on this example: http://www.knockmeout.net/2011/05/creating-smart-dirty-flag-in-knockoutjs.html
It does exactly what I want, except for a single use case that I don't know how to solve.
Basically I have a select menu that gets automatically populated from the database. This select menu also has an option to make an Ajax call to my back end and have the list of options refreshed, database updated and return the result. This is where things get hairy for me.
First method works fine, however, it has to re-index and re-apply my entire viewModel and takes about 2-3 seconds, running on a local machine with 16gigs of ram and SSD.
jsondata.component.available_tags = result.available_tags;
ko.mapping.fromJS(jsondata, viewModel);
Second method also works, and pretty much instantaneous, however, it sets of isDirty() flag, which I would like to avoid, because this data is already coming from the database and I wont need to save it. I can not use isDirty.reset() method either, because if isDirty was set by something else before I clicked an menu option to update available_tags, it will reset that too. Which I would also like to avoid.
viewModel().component.available_tags(result.available_tags);
My question is: With the first method, can I force UI refresh with ko.mapping.fromJS() on a particular element and not entire dataset? Or, with a second method, can I avoid setting isDirty flag set when available_tags are updated? The twist is that I still need to keep available_tags as an observable, so the select menu is automatically generate/updated.
UPDATE: I was able to update mapping for that one single element with
ko.mapping.fromJS(result.available_tags, {}, viewModel().component.available_tags);
but that immediately set off isDirty flag... Argh
In addition to Tomalak's suggestions, which I totally agree with, maybe the toJSON method can help you out in similar cases where you don't want to split the model. If your dirty flag implementation uses ko.toJSON as a hash function, as Ryan Niemeyer's does, you can give your model (on which the dirty flag is active) a toJSON method, where you do something like this:
function MyObjectConstructor() {
this.someProperty = ko.observable();
this.somePropertyNotUsedInDirtyFlag = ko.observable();
}
MyObjectConstructor.prototype.toJSON = function () {
var result = ko.toJS(this);
delete result.somePropertyNotUsedInDirtyFlag;
return result;
};
Please be aware that this is also used to serialize the object in some other occassions, such as ajax calls. It's generally a handy function for removing computeds and such from your objects before using them in a different context.

Knockout - Updating observableArray in View Model

I wish to save my view model to a database as a JSON string. The idea is that I can then re-load my view model by reading the JSON back and using the direct approach to load my view model's data:
From the Knockout documentation:
// Load and parse the JSON
var someJSON = /* Omitted: fetch it from the server however you want */;
var parsed = JSON.parse(someJSON);
// Update view model properties
viewModel.firstName(parsed.firstName);
viewModel.pets(parsed.pets);
That all works great but where I've already initialised my model and I'm simply updating it with one that I've already saved, I can't see how I can select the originally selected entry in the array's drop-down list.
To put it another way, the pets drop-down list is selected as "Cat" when I save my model. I then change the drop-down list selection to "Dog". On re-loading the saved model, I need my drop-down list selection to be reset to "Cat".
I'm a bit concerned about this because I have some arrays of objects which also need to be read in from the saved model and it's looking like it's going to be very difficult to do.
Any ideas or suggestions are welcome :)
What you are looking for is the mapping plugin for Knockout. http://knockoutjs.com/documentation/plugins-mapping.html
It has methods that handle both JSON to observables and back.
So in your example you could do:
var viewModel = ko.mapping.fromJSON(someJSON);
And when you're ready to go back to the server:
var jsonData = ko.mapping.toJSON(viewModel);
There are also object literal helpers if you need that (ko.mapping.toJS & ko.mapping.fromJS)

AngularJS binding between variables

I've got a list of users which I retrieve from my service. When I select any user I can see and edit info (email, roles, etc). The problem is that I don't want these changes to affect user's data in the list, I want to update data only after saving (clicking a button).
Now I'm using two variables:
$scope.selected - currently selected user
$scope.editable - variable for storing the data I'm editing
And I exchange data like this:
$scope.initEditable = function ()
{
$scope.editable = {};
$.extend($scope.editable, $scope.selected);
}
Looks like a terrible solution. What is the proper way to do it?
Actually, this is the Angular-way of approaching this problem, you are on the right track. In scenarios like yours one would typically:
Copy an item upon selection (edit start) - this is what you do with editable
Have 2-way data binding changing a copy (or an original element)
Upon edit completion we can propagate changes from a copy to the original
The nice things about this pattern is that we can easily:
Offer the 'cancel' functionality where users can revert their changes
Be able to compare a copy and the original and drive parts of the UI based on this comparison (for example, we could disable a 'Save' button if there were no changes)
So, I don't think at all that this approach is terrible. The only suggestion I could have is to use angular's angular.copy method instead of $.extend.

KnockoutJS: Update/Insert data to a viewModel using mapping

I've been trying to figure this out for quite some time now. I couldn't find anything that addresses this problem, but please correct me if I'm wrong.
The problem:
I have data from a JSON API comming in, with an nested array/object structure. I use mapping to initially fill the model with my data. To update this, I want to extend the model if new data arrives, or update the existing data.
As far as I found out, the mapping option key, should do this trick for me, but I might have misunderstood the functionality of the mapping options.
I've boiled down the problem to be represented by this example:
var userMapping = {
key: function(item) {
return ko.utils.unwrapObservable(item.id);
}
};
// JSON call replaced with values
var viewModel = {
users: ko.mapping.fromJS([], userMapping)
};
// Should insert new - new ID?
ko.mapping.fromJS([{"id":1,"name":"Foo"}, {"id":2,"name":"Bar"}], userMapping, viewModel.users);
// Should only update ID#1 - same ID?
ko.mapping.fromJS([{"id":1,"name":"Bat"}], userMapping, viewModel.users);
// Should insert new - New ID?
ko.mapping.fromJS([{"id":3,"name":"New"}, {"id":4,"name":"New"}], userMapping, viewModel.users);
ko.applyBindings(viewModel);​
Fiddle: http://jsfiddle.net/mikaelbr/gDjA7/
As you can see, the first line inserts the data. All good. But when I try to update, it replaces the content. The same for the third mapping; it replaces the content, instead of extening it.
Am I using it wrong? Should I try to extend the content "manually" before using mapping?
Edit Solution:
I solved this case by having a second helper array storing all current models. On new data i extended this array, and updated the view model to contain the accumulated items.
On update (In my case a WebSocket message), I looped through the models, changed the contents of the item in question, and used method valueHasMutated() to give notice of changed value to the Knockout lib.
From looking at your example code the mapping plugin is behaving exactly as I would expect it to. When you call fromJS on a collection you are effectively telling the mapping plugin this is the new contents of that collection. For example:
On the second line, How could it know whether you were updating or whether you had simply removed id:2?
I can't find any mention of a suitable method that treats the data as simply an update, although you could add one. Mapped arrays come with some helpful methods such as mappedIndexOf to help you find particular items. If you receive an update data set simply loop through it, find the item and update it with a mapping.fromJS call to that particular item. This can easily be generalized into reusable method.
You can use ko.mapping.updateFromJS() to update existing values. However, it does not add new values so that would be a problem in your instance. Take a look at the link below for more details.
Using updateFromJS is replacing values when it should be adding them
Yes, you should first collect all data into a list or array and then apply the mapping to that list. Otherwise you are going to overwrite the values in your viewModel.

Categories