2

I have a list of clients displayed through a ClientsController, its content is set to the Client.find() i.e. a RecordArray. User creates a new client through a ClientController whose content is set to Client.createRecord() in the route handler.

All works fine, however, while the user fills up the client's creation form, the clients list gets updated with the new client record, the one created in the route handler.

What's the best way to make RecordArray/Store only aware of the new record until the record is saved ?

UPDATE: I ended up filtering the list based on the object status

{{#unless item.isNew}} Display the list {{/unless}}

UPDATE - 2 Here's an alternative way using filter, however the store has to be loaded first through the find method, App.Client.find().filter() doesn't seem to behave the way the two methods behave when called separately.

// Load the store first     
App.Client.find();
    var clients = App.Client.filter(function(client){            
        console.info(client.get('name') + ' ' + client.get('isNew'));  
        return !client.get('isNew');
    });                   
    controller.set('content',clients);  
ken
  • 3,745
  • 6
  • 34
  • 49
  • are you saying that once the user's filled out the form, you grab the properties from the filled out model object and manually send them to the server, rather than using `commit`? – Alexander Wallace Matchneer Jan 12 '13 at 11:15
  • When the user fills out the form, value are already bound to the model created previously though createRecord and set to the controller's content. On submit, all you need to do is store.commit, on Cancel, you can rollback. The record belongs already to a defaultTransaction when none is specified. – ken Jan 12 '13 at 15:24
  • 1
    See the comment below my answer. You can avoid a lot of messy issues by creating your own managed transaction() object. If you get in the habit of creating your own transaction() objects, you can avoid a lot of weird problems that are likely to sneak up as your app grows. – Alexander Wallace Matchneer Jan 12 '13 at 15:36

2 Answers2

2

Few ways to go about this:

First, it's very messy for a route/state that deals with a list of clients to have to go out of its way to filter out junk left over from another unrelated state (i.e. the newClient state). I think it'd be way better for you to delete the junk record before leaving the newClient state, a la

if(client.get("isNew")) {
  client.deleteRecord();
}

This will make sure it doesn't creep into the clientIndex route, or any other client list route that shouldn't have to put in extra work to filter out junk records. This code would ideally sit in the exit function of your newClient route so it can delete the record before the router transitions to another state that'll called Client.find()

But there's an even better, idiomatic solution: https://gist.github.com/4512271

(not sure which version of the router you're using but this is applicable to both)

The solution is to use transactions: instead of calling createRecord() directly on Client, call createRecord() on the transaction, so that the new client record is associated with that transaction, and then all you need to do is call transaction.rollback() in exit -- you don't even need to call isNew on anything, if the client record was saved, it obviously won't be rolled back.

This is also a useful pattern for editing records: 1) create a transaction on enter state and add the record to it, e.g.

enter: function(router, client) {
     this.tx = router.get("store").transaction();
     this.tx.add(client); 
},

then the same sort of thing on the exit state:

exit: function(router, client) {
     this.tx.rollback();
},

This way, if the user completes the form and submits to the server, rollback will correctly/conveniently do nothing. And if the user edits some of the form fields but then backs out halfway through, your exit callback will revert the unsaved changes, so that you don't end up with some dirty zombie client popping up in your clientIndex routes display it's unsaved changes.

  • When you call createRecord, the record will be attached to a "defaultTransaction". The only reason i'm creating the record is to set it the client controller's content so that when the user fills up the form, the inputs are bound the model properties. If you attempt rather to create an object through the create method, ember-data prevent you, throwing an exception. Frankly that's what i need, only to create a simple object, bind to it and on submit create a record and commit the transaction. – ken Jan 12 '13 at 15:19
  • `defaultTransacation` is messier since it get can get mixed up with other records whose properties might have been changed, putting them into a dirty state and attaching them to the same defaultTransaction. If you do it the way I described, you'll have a well-encapsulated transaction only for the Client in question, and you're guaranteed to not run into issues with other models accidentally being on the same transaction. If you don't want to use `.commit()` on the model to save to the server, you can retrieve an object of properties from the client via `client.getProperties(["foo", "bar"])` – Alexander Wallace Matchneer Jan 12 '13 at 15:34
  • I was just looking at your gist again. I think you have a point, i will try it. Out of curiosity, why would you implement the whole transaction handling in the route rather than in the controller ? I handle my submit event in the controller client controller. – ken Jan 12 '13 at 16:06
  • Also getProperties would be a good fit if it were a Class method that extracts a simple object out of the record, it's not the case though, how is that useful ?. Mind you i can always write one myself. – ken Jan 12 '13 at 16:13
  • If you have a `client` object that's bound to a form that the user's filling out, and you want to retrieve an object with the client's `first_name`, `last_name`, `city`, you can run `client.getProperties(['first_name', 'last_name', 'city'])`. It's not a class method, and it shouldn't be, but it'll do exactly what you say: extract a simple object out of the record that you can easily send in via Ajax. – Alexander Wallace Matchneer Jan 12 '13 at 16:22
  • 1
    RE: previous question, you can perform the `transaction.commit()` in a controller when you need to send it in, but if you're leaving the `newClient` route, the router's the best place to put the rollback logic since it'll catch any and every transition out of the `newClient` route and properly clean up after itself. The ember guys are wise to push State Machine design on us as much as they do, and the router's no exception. – Alexander Wallace Matchneer Jan 12 '13 at 16:23
  • I definitely agree with you, i should benefit more from the state machine design. The newClient state should be a sub state in my case, as i start with a state that list all the clients. The only thing that I need to find out is how to move form a state to a substate and back, keeping in my mind that in my scenario when i transition to the substate newClient, i need to create a view popup it up in a modal and when i close/destroy the view i have to transition back to the clients state. – ken Jan 12 '13 at 16:48
  • Alexander, i owe you buddy for drawing my attention to the use of transactions and better leverage of the state machine. i converted my code to deal with the popup in the edit and new modes to two sub-states for the parent state clients. It works just great. Frankly i realized how entangled and messy my code was, now it's superb. I found myself looking at this again https://speakerdeck.com/trek/applications-a-series-of-states and validating each piece of it. Thx – ken Jan 13 '13 at 00:06
  • @ken check out this gist: https://gist.github.com/4520011 . similar situation: i needed to display a modal that didn't really belong in the main Router (in my case, the modal could be launched from a few different pages, and shouldn't be externally linkable, and didn't need to change the URL). even if it's not by using the Router (a subclass of StateManager), you can always roll out your own state manager when you need it. – Alexander Wallace Matchneer Jan 13 '13 at 00:55
  • Cool, not familiar with coffee script, do you have the javascript version handy ? How did you prevent the url change, I also was looking for a way to prevent it for sub-states. Looking at your code, there is nothing specific about it, so i suppose you just don't make an explicit call to change it, whereas in the router code base, there is an explicit call to updateUrl. I was sifting through the Emberjs source code to see if there is a way to prevent it at the Route level. – ken Jan 13 '13 at 04:09
1

Not 100% sure, could you try to set the content of ClientsController with

Client.filter(function(client){
  return !client.get('isNew'));
});

EDIT: In order to make this work, you have to first load the store with Client.find().

sly7_7
  • 11,961
  • 3
  • 40
  • 54
  • Added another update. Your first suggestion works, however you need load the store first. You should update your answer for better accuracy. Thx. – ken Jan 10 '13 at 00:05
  • Yeah that what I mean by it doesn't work. Out of curiosity, I think you populate the ClientsController in the model() function of the route. So at what moment do you load the store first ? – sly7_7 Jan 10 '13 at 00:11
  • That's what i'm doing right now, I only implement the model method in the route handler. The drawback of this, however, is that i hit the server each time i visit /clients. Alternatively, I can load the store only once, when the controller is created (Not fond of) or in the model method by checking the store status if there is any. Can you think of a better way ? – ken Jan 10 '13 at 00:26
  • 1
    I think the check in the model method should be a good workaround. Then when https://github.com/emberjs/data/issues/599 will be implemented, you will just have to remove this small hack – sly7_7 Jan 10 '13 at 00:31
  • Sweet, the way it's described in the issue is the proper way to doing this due to the nature of "browser applications". I might as well just override the find method and implement the hack in there. – ken Jan 10 '13 at 00:58
  • Turned out, the solution is only viable when we're dealing with new records, however when we try to edit a record, we can see the changes in the list while doing them in the popup. Filtering dirty item (isDirty = true) won't help, the item will be removed from the list at once ... Not sure how to prevent this from happening ... Any idea ? – ken Jan 10 '13 at 01:53
  • Erf, I can't realize exactly what's you expect. For editing, perhaps simply using the {{unless isDirty}} in the template would do the job ? – sly7_7 Jan 10 '13 at 10:13
  • A conditional won't work here either. As i mentioned in my comment, the whole item will be filtered from the list the moment it gets dirty. Somehow I need to keep the ArrayRecord unaware of the changes until i decide so ... – ken Jan 10 '13 at 15:56
  • The only thing I think about, is to fill the edition form with an Ember object clone of the model. Then when you save, you go through the cloned object, set its properties into the model, and finally commit it. – sly7_7 Jan 10 '13 at 23:46