Sencha extjs model.erase removes model even when server error - extjs

When calling model.erase({failure..., success...}) the model is removed even when the server responds with a HTTP StatusCode 500. The failure listener is triggered correctly but i would expect that model is not destroyed then. I can see that it is destroyed because it gets removed from the store.
var rec = store.getAt(index);
rec.erase({
success:function(record, operation){
// Do something to notify user knows
}
failure:function(record, operation){
// correctly triggered when HTTP = 40x or 50x
// Would expect that record is still in store. Why not?
// Of course i could add it again to store with store.add(record) but is that the prefered way?
}
});
I am using an AJAX proxy in Extjs 6.0

Yes, the erase method removes the record from the store right away, without waiting for the server's response. The "hacky" way to handles you scenario will be:
set the record's dropped property to true;
save the record using the save method (it will generate a delete request but will keep the record in the store);
remove the record from the store on success, reset the dropped property to false of failure.
var record = store.getAt(index);
record.dropped = true;
record.save({
success: function() {
store.remove(record);
// do something to notify the user
}
failure: function() {
record.dropped = false;
}
});

The erase isn't really relevant here. Calling erase calls the model drop method, which marks it as pending deletion and removes it from any stores. Just because the server failed to delete it from the server doesn't necessarily mean you want it back in the store, it's still just pending deletion.

Related

How to force ngrx-data to clear cashed entities and reload data from db

I have a typical ngrx-data arrangement of 'User' entities linked to db.
I implement the standard service to handle the data:
#Injectable({providedIn: 'root'})
export class UserService extends EntityCollectionServiceBase<UserEntity> {
constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
super('User', serviceElementsFactory);
}
}
I read the data using:
this.data$ = this.userService.getAll();
this.data$.subscribe(d => { this.data = d; ... }
Data arrives fine. Now, I have a GUI / HTML form where user can make changes and update them. It also works fine. Any changes user makes in the form are updated via:
this.data[fieldName] = newValue;
This updates the data and ngrx-data automatically updates the entity cache.
I want to implement an option, where user can decide to cancel all changes before they are written to the db, and get the initial data before he made any adjustments. However, I am somehow unable to overwrite the cached changes.
I tried:
this.userService.clearCache();
this.userService.load();
also tried to re-call:
this.data$ = this.userService.getAll();
but I am constantly getting the data from the cache that has been changed by the user, not the data from the db. In the db I see the data not modified. No steps were taken to write the data to db.
I am not able to find the approach to discard my entity cache and reload the original db data to replace the cached values.
Any input is appreciated.
You will need to subscribe to the reassigned observable when you change this.data$, but it will be a bit messy.
First you bind this.data$ via this.data$ = this.userService.entities$, then no matter you use load() or getAll(), as long as the entities$ changed, it fire to this.data$.subscribe(). You can even skip the load and getAll if you already did that in other process.
You can then use the clearCache() then load() to reset the cache.
But I strongly recommand you to keep the entity data pure. If the user exit in the middle without save or reset, the data is changed everywhere you use this entity.
My recommand alternatives:
(1) Use angular FormGroup to set the form data with entity data value, then use this setting function to reset the form.
(2) Make a function to copy the data, then use this copy function as reset.
For example using _.cloneDeep.
(2.1) Using rxjs BehaviourSubject:
resetTrigger$ = new BehaviourSubject<boolean>(false);
ngOnInit(){
this.data$ = combineLastest([
this.resetTrigger$,
this.userService.entities$
]).subscribe([trigger, data])=>{
this.data = _.cloneDeep(data)
});
// can skip if already loaded before
this.userService.load();
}
When you want to reset the data, set a new value to the trigger
resetForm(){
this.resetTrigger$.next(!this.resetTrigger$.value)
}
(2.2) Using native function (need to store the original):
this.data$ = this.userService.entities$.pipe(
tap(d=>{
this.originData = d;
resetForm();
})
).subscribe()
resetForm:
resetForm:()=>{
this.data = _.cloneDeep(this.originData);
}

Publish/Subscribe not working automatically when data added to the mongodb

I have the following publisher and subscriber code.
It works for the first time when the app starts, but when I try to insert data directly into the Mongo database, it will not automatically update the user screen or I don't see the alert popping.
Am I missing something?
Publish
Meteor.publish('userConnections', function(){
if(!this.userId){
return;
}
return Connections.find({userId: this.userId});
})
Subscribe
$scope.$meteorSubscribe('userConnections').then(function () {
var userContacts = $scope.$meteorCollection(Connections);
alert("subscriber userConnections is called");
if (userContacts && userContacts[0]) {
....
}
}, false);
First off, if you are not using angular-meteor 1.3 you should be. The API has changed a lot. $meteorSubscribe has been deprecated!
To directly answer your question, $meteorSubscribe is a promise that gets resolved (only once) when the subscription is ready. So, it will only ever be called once. If you look at the documentation for subscribe you'll see how to make the binding "reactive", by assigning it to a scope variable. In your case it would be something like:
$scope.userContacts = $scope.$meteorCollection(Connections);
Doing it this way, when the collection gets updated, the $scope.userContacts should get updated as well.

$scope not updating after passing to new view with $location.path

I have a simple CRUD I put together with Angularjs. From a product list display I pass the user to a new view template for the "Create New" form.
The form processes fine and updates the database. I then pass the user back to the list display using "$location.path(url)".
For some reason when the list page displays, the changes do not appear in the $scope and you have to refresh the page to see the changes.
Sample code:
$scope.acns = acnFactory.query()
.$promise.then(function (data) {
$scope.acns = data;
});
the above displays the list of items.
$scope.createAcn = function () {
$scope.acn.isActive = true;
acnFactory.create($scope.acn);
$location.path('/acn');
}
The above POSTs the new product then redirects to the list page (/acn)
My assumption is that the list page will reprocess or watch the changes to the $scope but the view does not update.
The problem is most probably here:
$scope.createAcn = function () {
$scope.acn.isActive = true;
acnFactory.create($scope.acn);
$location.path('/acn');
}
creating a product most certainly consists in sending an HTTP request to the server. This is asynchronous. This means that acnFactory.create() returns immediately after the request has been sent. Not after the response has been received.
So, this code sends the request to create the product and immediately goes to the page which lists the products. The GET request sent to get the product list is thus sent almost at the same instant as the one used to create the new product. The two requests are handled concurrently by the server, and the returned list contains the list without the new product, which is being created in a separate transaction.
You need to wait for the response to come back, and make sure it's successful, before going to the product list. Assuming the service returns a promise, as it should do:
$scope.createAcn = function () {
$scope.acn.isActive = true;
acnFactory.create($scope.acn).then(function() {
$location.path('/acn');
});
};

Checking dynamic amount of deferreds of for example file uploads

Context
The situation as follows: Users can upload files in an application. They can do this at any time (and number of times).
I would like to show a spinner when any uploading is being done, and remove it when no uploading is happening at the moment.
My approach
The uploads are handles by an external file upload plugin (like blueimp) and on it's add method I grab the jqXHR object and add these to a backbone collection (which are images in my application, so I use this in combination with Marionette's collectionviews).
The following is part of a function called in an onRender callback of a Marionette Itemview:
// Get the file collection
var uploadFiles = SomeBackBoneCollection;
// Track how many deferreds are expected to finish
var expected = 0;
// When an image is added, get the jqXHR object
uploadFiles.bind('add', function(model) {
// Get jqXHR object and call function which tracks it
trackUploads(model.get('jqXHR'));
// Do something to show the spinner
console.log('start the spinner!');
// Track amount of active deferreds
expected++;
}, this);
// Track the uploads
function trackUploads(jqXHR) {
$.when(jqXHR).done(function(){
// A deferred has resolved, subtract it
expected--;
// If we have no more active requests, remove the spinner
if (expected === 0) {
console.log('disable the spinner!');
}
});
}
Discussion
This method works very well, although I'm wondering if there are any other (better) approaches.
What do you think about this method? Regarding this method, do you see any up- or downsides? Any other methods or suggestions anyone?
For example, it might be great to have some kind of array/object to which you can keep passing deferreds, and that a $.when is somehow monitoring this collection and resolves if at any moment everything is done. However, this should work such that you can keep passing deferred objects at any given time.
you can do this via events.
I am assuming each file is an instance of this model:
App.Models.File = Backbone.Model.extend({});
before the user upload the file, you are actually creating a new model, and save it.
uploadedFiles.create(new App.Models.File({...}));
so in your upload view...
//listen to collection events
initialize: function() {
//'request' is triggered when an ajax request is sent
this.listenTo(this.collection, 'request', this.renderSpinner);
//when the model is saved, sync will be triggered
this.listenTo(this.collection, 'sync', this.handleCollectionSync);
}
renderSpinner: function() {
//show the spinner if it is not already being shown.
}
ok, so, in 'handleCollectionSync' function, you want to decide if we wanna hide the spinner.
so how do we know if there're still models being uploaded? you check if there're new models in the collection (not saved models)
so in your collection, add a helper method:
App.Collections.Files = Backbone.Collection.extend({
//if there's a new model, return true
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.isNew();
}).length > 0;
}
});
back to your view:
handleCollectionSync: function() {
//if there's no unsaved models
if(!this.collection.hasUnsavedModels()){
//removespinner
}
}
this should solve your problem assuming all the uploads are successful. you may want to complete this with error handling cases - it depends on what you wanna do with error case, but as long as you are not retrying it right away, you should remove it from the collection.
==========================================================================================
Edit
I'm thinking, if you allow the user to upload a file multiple times, you are not really creating new models, but updating existing ones, so the previous answer would not work. to work around this, I would track the status on the model itself.
App.Models.File = Backbone.Model.extend({
initialize: function() {
this.uploading = false; //default state
this.on('request', this.setUploading);
this.on('sync error', this.clearUploading);
}
});
then setUploading method should set uploading to true, clearUploading should change it to false;
and in your collection:
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.uploading;
}).length > 0;
}
so in your view, when you create a new file
uploadNewFile: function(fileAttributes) {
var newFile = new App.Model.File(fileAttributes);
this.collection.add(newFile);
newFile.save();
}
I believe 'sync' and 'request' events are triggered on the collection too when you save models inside of it. so you can still listenTo request, sync, and error events on the collection, in the view.

Backbone.Model.save does not set my model with the server response

I'm calling 'save' on my model and returning the new model as json in my PHP backend. When I step through the Backbone.Model.save method, I can see that it successfully gets the server response and then sets the model (in the options.success closure below). But when the execution is returned to my click handler, the model has the old properties (ie. the id is not set). What could be happening?
Here is my click handler:
addButtonClick: function(e) {
var data = $(e.target).closest('form').serializeObject();
var p = new Domain.Page; // this extends Backbone.Model
p.set(data);
p.save();
// ****
// **** AFTER p.save, the model p still has the OLD ATTRIBUTES... why??
// ****
return false;
}
Here is Backbone's save method:
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save : function(attrs, options) {
options || (options = {});
if (attrs && !this.set(attrs, options)) return false;
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
// ****
// **** NEXT LINE SUCCESSFULLY SETS THE MODEL WITH THE SERVER RESPONSE
// ****
if (!model.set(model.parse(resp, xhr), options)) return false;
if (success) success(model, resp, xhr);
};
options.error = wrapError(options.error, model, options);
var method = this.isNew() ? 'create' : 'update';
return (this.sync || Backbone.sync).call(this, method, this, options);
},
The save method is asynchronous. In other words, the model.set call inside save happens after the server has responded.
You are asking why the values are the same immediately after save is called. The answer is: at that point in time, the response has not been received by your code yet. The callback hasn't been called. model.set hasn't been called.
When you continue on and the event loop gets the response from the server (this may be a fraction of a second, it may be several seconds) later, your values will get set.
I think I figured out what was wrong here. And Brian you were right to say it had something to do with the async nature of the Backbone.save call. The thing is that I was using the DEBUGGER. This stops all execution. I actually don't really understand how an async call works under the hood, perhaps with threads? I assumed that after I stepped over the call to 'save' and then waited a sec, then the async part (whatever that is) of the 'save' call would execute in the background. But this is not the case. The debugger halts everything. So the options.success closure within 'save' always gets called sometime after stepping over 'save'. In short, this whole thing is due to me not understanding javascript and javascript debugging properly.

Resources