Ember-data transactions and resubmitting forms - ember.js

I'm using ember(+data) and have built a simple signup form, but I'm having trouble getting the form (and ember-data transaction) to work properly after a server-side validation error.
I'm following https://github.com/dgeb/ember_data_example as a guide for how to use transactions, and it mostly works for the Happy Path, but after a server-side validation error, I can't get ember-data to resubmit the API request when I click Submit.
I did some digging around, and I think I'm missing some step related to resetting the transaction after the model becomes invalid or something like that...
Github and App
You can try the app and reproduce the problem at http://emb.herokuapp.com/
You can also examine the full source code at https://github.com/justinfaulkner/ember-data-resubmit
Repro
You can trigger a fake server-side error by completing the form in my app using an e-mail address at #example.com. The Rails API will respond 422 and an errors object keyed to the username/email field.
The field should be highlighted in red with an error -- edit the email address to something that should be valid, and click Submit again. With developer tools open in chrome, I don't see ember-data sending an http request with the click (but a console.log in the controller indicates that the click is indeed being received).
What should happen
After modifying the field with the error, ember-data (I think) changes the model's from invalid to uncommitted so that it gets submitted the next time commit is called. When I click Submit, ember-data should send the HTTP request to my api, and ember should transition to the "Congrats!" page.
Instead, however, the form just sits there. No http request. No transition to "Congrats!".
Here's a screenshot after I clicked Submit a few (18) times after having updated the inputs:
Snippets
Here are some snippets of my ember app:
IndexRoute
My route, which uses startEditing like ember_data_example does:
App.IndexRoute = Ember.Route.extend({
model: function() {
return null;
},
setupController: function(controller) {
controller.startEditing();
},
deactivate: function() {
this.controllerFor('index').stopEditing();
}
});
IndexController
My IndexController is modeled after ember_data_example's ContactsNewController
App.IndexController = Ember.ObjectController.extend({
startEditing: function() {
this.transaction = this.get('store').transaction();
this.set('content', this.transaction.createRecord(App.User));
},
submit: function(user){
console.log("submitting!");
this.transaction.commit();
this.transaction = null;
},
_transitionOnSuccess: function(stuff) {
if (this.get('content.id') && this.get('content.id').length > 0) {
console.log("_transitionOnSuccess");
this.transitionToRoute('success');
}
}.observes('content.id'),
stopEditing: function() {
if (this.transaction) {
this.transaction.rollback();
this.transaction = null;
}
}
});
User
Here's my model. I'm using ember-validations.
App.User = DS.Model.extend(Ember.Validations.Mixin);
App.User.reopen({
username: DS.attr('string'),
password: DS.attr('string'),
profile: DS.belongsTo('App.Profile'),
validations: {
username: {
presence: true
},
password: {
presence: true,
length: { minimum: 6 }
}
}
});
index.hbs
And here's the form from my handlebars template. I'm using ember-easyForm.
{{#formFor controller}}
<div class="row">
<div class="large-12 columns">
{{input username placeholder="Email address"}}
</div>
</div>
<div class="row">
<div class="large-12 columns">
{{input password placeholder="Password"}}
</div>
</div>
{{submit "Sign Up" class="button"}}
{{/formFor}}
In case the library authors see this post, I have made a couple local modifications to the app's copy of ember-easyForm and ember-validations in this commit: https://github.com/justinfaulkner/dockyard-example/commit/f618b0e4fb72314d56bb3a9d95e1325925ba6ad0 . I don't think my changes are causing my problem, though.
becameInvalid and rollback?
I did run across a similar-looking question here: Ember Data and dirty records but when I added a User.becameInvalid to rollback the transaction, this caused the form to empty when trying to re-submit (and still no success having ember-data resubmit the http request).
Thanks!
I'm sure I'm following ember_data_example poorly (or failing to extend it to my use-case) or am making some simple mistake somewhere...
Thanks in advance.
Edit Apr 5
So far, I can find at least two main problems:
this.transaction = null; Do I need this? What should I do instead?
I tried removing this.transaction = null; and ember-data tries to actually commit now (but still won't submit the ajax request). Now, when I type in the invalid field, I can see ember-data try to update the record back to uncommitted/created... but it does it in a different transaction.
I pushed up a no-null branch to the repo that has some console.logs in ember-data that prints out what the transaction ID is... here's a snapshot of my console:
recordBecameDirty is called when I type in the field. Ember-data updates the record to be ready to be committable again. But, it's doing it in some other transaction (458)
But my submit button is tied to the original transaction (316), and there's no records ready to commit in that transaction.... hmm....

This is a known issue with ember data.
See: https://github.com/emberjs/data/pull/539
The simple change in Ember Data in that commit
https://github.com/Cyril-sf/data/commit/fe9c63beb02e9f16051e59a9f7c0a918152a0231
should solve your problem.

Related

Ember DjangoRESTAdapter data not loading

I'm pretty new to Ember so hopefully I'm just doing something stupid, but I've been running into a lot of random issues with data not displaying properly and I now see in the Ember Debugger that my data does not exist until I hit a specific model data endpoint. For instance, I have a template to display all products, here's the route:
App.ProductsRoute = Ember.Route.extend({
model: function() {
return this.store.find('product');
}
});
the controller:
App.ProductsController = Ember.ArrayController.extend({
itemController: 'product'
});
the template:
<script type="text/x-handlebars" data-template-name="products">
<div class="row">
{{#each}}
{{name}}
{{/each}}
</div>
</script>
Hitting the endpoint '/products' displays nothing initially, but if I go to '/products/1' I can see the product data in the view (and in the Ember debugger), and then if I navigate back to '/products' that particular product's data (but no other data) displays properly. So I'm super confused as to what I'm doing wrong. As the title suggests, I'm using the DjangoRESTAdapter if that helps narrow things down and here's my app.js as well
window.App = Ember.Application.create({});
window.api_location = 'http://localhost:8000/api';
App.ApplicationAdapter = DS.DjangoRESTAdapter.extend({
host: api_location,
pathForType: function(type) {
return Ember.String.underscore(type);
}
});
App.ApplicationSerializer = DS.DjangoRESTSerializer.extend({
});
App.Store = DS.Store.extend();
Thanks in advance for any help, and let me know if other code snippets would help.
Okay, I finally figured this out: it's an issue with pagination. I was not aware that we had pagination set up for our Django REST Api, so instead of returning a list of objects, it was returning a result object, with the list of products buried in a 'results' attribute. So until I can convince the other devs to turn off pagination I can modify all my queries:
this.store.find('product', {page_size:0});
to override our default page size.
Edit: I'm also trying out modifying the json response on the server side rather than using the djangorestadapter with this library: https://github.com/ngenworks/rest_framework_ember . Hopefully this saves some people some digging...

Ember Navbar needs to show model data upon change

I am working on an application that needs to modify the content of the navbar after login. Here's a basic sketch I put together (with some help from other samples online):
http://jsbin.com/umutag/1/ with this underlying code: http://jsbin.com/umutag/1/edit
How do I get the header view to display model data?
Should I be using a different helper for the template? (e.g. a {{view}}, {{render}}, or {{control}})
BTW, I've scoured this site and others, but most entries are a few months old and I see ember has been changing a lot since then (or I'm missing something obvious). The above example uses Ember 1.0.0 RC6.
Bryan
You ultimately want to bind the value in the controller (probably ApplicationController) that keeps track of whether the user is logged in or not. Since this is pertaining to login, you most likely have something like a SessionController that keeps track of the token. Here's one way to go about it:
App.SessionController = Em.Controller.extend({
token: null,
username: null,
isLoggedIn: function() {
return !!this.get("token");
}.property("token");
// ...
});
App.ApplicationController = Em.Controller.extend({
needs: "session",
isLoggedInBinding: "controllers.session.isLoggedIn",
usernameBinding: "controllers.session.username"
//...
});
And in your navbar in the template:
{{#if isLoggedIn}}
<li>Logged in as {{username}}</li>
<li>{{#linkTo "index"}}Home{{/linkTo}}</li>
<li>{{#linkTo "secret"}}Secret{{/linkTo}}</li>
{{else}}
<li>{{#linkTo "login"}}Log in{{/linkTo}}</li>
{{/if}}

ember.js - unable to commit form data

I'm relatively new to ember.js and still having a painful time learning it. I am using the ember-rails gem, and Rails as a JSON API.
First I just wanted to build something that would display a list of records (in this case, emails) with the ability to add new ones. I have the UI setup and functioning, and ember is pulling the list of emails from the Rails API. I can click an email title and it takes me to the full email etc.
My problem is adding new records. I have a Compose Email form set up, but I still have not managed to add a new record, after several days of re-arranging code in routers and controllers. Every example I have found seems to suggest a different way of juggling transactions, createRecords, .get('store'), .get('content') and then committing the details through an action of some sort. None of them have worked for me. Here's what feels like the closest I've got:
Store
App.Adapter = DS.RESTAdapter.extend
serializer: DS.RESTSerializer.extend
primaryKey: (type) -> '_id'
App.Store = DS.Store.extend
revision: 11
adapter: 'App.Adapter'
Email model
App.Email = DS.Model.extend
from: DS.attr('string')
to: DS.attr('string')
subject: DS.attr('string')
body: DS.attr('string')
created_at: DS.attr('string')
Compose Route
App.ComposeRoute = Ember.Route.extend
model: () ->
transaction = this.get('store').transaction()
email = transaction.createRecord(App.Email, {})
return email
events: {
save: (email) ->
email.get('store').transaction()
}
Compose Template (trimmed of some HTML)
<label class="control-label" for="to">To</label>
{{view Ember.TextField valueBinding="to" id="to" placeholder="Your friend" required="true"}}
<label class="control-label" for="subject">Subject</label>
{{view Ember.TextField valueBinding="subject" id="subject" placeholder="What's it about?" required="true"}}
<label class="control-label" for="notes">Your message</label>
{{view Ember.TextArea valueBinding="body" id="body" placeholder="Your message"}}
<button class="btn btn-primary btn-large" {{action save this}}>Send email</button>
Rails emails_controller.rb
class EmailsController < ApplicationController
# GET /emails.json
def index
render json: Email.all
end
# GET /emails/1.json
def show
email = Email.find(params[:id])
render json: email
end
# POST /emails.json
def create
email = Email.new(params[:email])
email.save
render json: email, status: :created
end
So with the above code I fill in the textboxes, click the submit button, and nothing seems to happen. However, if I go back to my Inbox page I can see the record I just added (woohoo!) but it's not persisted through Rails (hrmmm) - as soon as I refresh it's gone. Looking at the Rails logs, ember doesn't seem to be sending Rails anything.
I feel like I am going around in circles just trying different lines from various blog posts without really understanding the basic process. How should I be doing this? I've read through the Ember docs and while they explain the basic concepts, there are very few working examples using the RESTAdapter.
You never called commit anywhere in your code. Keep in mind that store.transaction() only gives you an instance of transaction, but you still have to commit (or rollback in Route#deactivate, if the user leaves that route). You might want to do your Route somewhat like this:
App.ComposeRoute = Ember.Route.extend
model: ->
transaction = this.get('store').transaction()
transaction.createRecord(App.Email, {})
deactivate: ->
## not super sure about modelFor in this case, but I think it should work
record = #modelFor('email')
## There's more to check here, like is record 'inflight' and all
## but you get the idea.
if ((record.get('isNew')) or (record.get('isDirty')))
#get('store.defaultTransaction').rollback()
events: {
save: (email) ->
#get('store.defaultTransaction').commit()
## or #get('store').commit()
}
The way you save your record is still not correct.
You create a new transaction for your email, it is this transaction you want to commit, not the store's one.
App.ComposeRoute = Ember.Route.extend
model: () ->
transaction = this.get('store').transaction()
email = transaction.createRecord(App.Email, {})
return email
events: {
save: (email) ->
// email.get('store').transaction() //This is only creating a new transaction
// #get('store.defaultTransaction').commit() // this is committing the store's transaction
email.get('transaction').commit()
}

Generating forms and handling submit properly with Ember.js rc1

I'm having trouble figuring out how to properly populate and accept an update from an Ember form under RC1. I've boiled it down to the bare essentials in this jsfiddle. I've made it far enough to display the form for a particular entity (user with first and last name) and the current values populate in the fields. However, as the user types, the fields actually update with each keystroke, and clicking the back button reveals that the data has already been changed without clicking the update button. I'd prefer to keep some logic in between the updates and only confirm an update after the user clicks the update button.
{{#view App.PersonFormView}}
First name: {{view Ember.TextField valueBinding="firstName"}}
Last name: {{view Ember.TextField valueBinding="lastName"}}
<button {{action "updatePerson"}}>Update</button>
{{/view}}
In the form template, I was trying to follow one of the Ember.js examples, but doing so resulted in a long delay and a monstrous deprecation warning using RC1. I think the examples are still being updated. I'd prefer a more handlebars-elegant way of coding the form if it existed.
The second problem is that I cannot capture the submit event itself, either on the form view or the controller. I don't know where this event is going.
App.PersonFormController = Ember.ObjectController.extend({
updatePerson: function(params){
// this doesn't get triggered as I would have expected
console.log('controller updatePerson: '+params);
}
});
App.PersonFormView = Ember.View.extend({
tagName: 'form',
updatePerson: function(params){
// this doesn't get triggered either!
console.log('updatePerson params: '+params);
}
});
In summary, I need to:
populate the input fields with the values without having them linked directly back to the model's data while the user is typing
catch the submit button's (or other control would be fine) clicked event along with the fields - and the entity's id - so that I can set them back on the model's data manually
There are several things:
I cannot capture the submit event itself
Events are fired in the controller and the route, not the view. The reason why your controller PersonFormController wasn't catching the event, is because the name is wrong. The controller should be named after the route: EditPersonController.
It's generally good to pass the model along with the action:
<button {{action "updatePerson" content}}>Update</button>
Here is an updated version that catches the event: http://jsfiddle.net/teddyzeenny/L9HMm/5/
populate the input fields with the values without having them linked directly back to the model's data
It's generally good practice to bind the fields directly to the model, to avoid code duplication.
Your problem is not that the fields are bound directly to the model, it's that you have no control over what is happening (saved, not saved, left the route...)
To have solid control, it's best to put your updating logic in your route. That way you can act accordingly when the user enters/leaves the route.
To catch your events in the route:
App.EditPersonRoute = Ember.Route.extend({
events: {
updatePerson: function(record) {
record.one('didUpdate', this, function() {
this.transitionTo('index');
});
record.get('transaction').commit();
}
}
});
To rollback changes if the user doesn't click on Update, use the deactivate callback in the route:
App.EditPersonRoute = Ember.Route.extend({
deactivate: function() {
this.modelFor('editPerson').get('transaction').rollback();
},
events: {
updatePerson: function(record) {
record.one('didUpdate', this, function() {
this.transitionTo('index');
});
record.get('transaction').commit();
}
}
});
Now these won't work in the fiddle since you are not using ember-data models.

How do I refresh a model in ember.js after I have added a record with createRecord()?

UPDATE: This is outdated, my blog is not Ember-based anymore. Basically, my question is simple. I added a record with createRecord(). I can see the didCreate event fired, but I don't know how to make ember load and display what I have just created. This case is about adding a comment to an post -- that is what I want to see instantly.
UPDATE: I have no jsfiddle, but I can show off the live app/site I am talking about is my own blog, here: http://eduardmoldovan.com/
The templates are at the bottom of the page, the javascript is here: http://eduardmoldovan.com/static/eduardmoldovan.com/javascripts/ngin.js
You can manually add your new record to array of records from the didCreate hook:
var newRecord = transaction.createRecord(Ngin.Comment, {
articleUrl: articleUrl,
name: name,
url: url,
email: email,
body: body,
secret: secret
});
newRecord.one('didCreate', this, function () {
this.get('comments').pushObject(newRecord);
});
transaction.commit();
Or, if you want to reload from server, use the reload method:
controller.get("comments").reload();
Edit
After examining sourcecode I found an update method in class RecordArray. It seems to be the right one.
Assuming you have used commit() to save the comment, something like this should work:
{{#each comment in post.comments}}
{{comment.text}}
{{/each}}
The following is neither efficient nor the best method but it will notify the view. And will try to find a better method. Im sure theres an update method and one that notifies the view. Cannot find it immediately.
template
<script type="text/x-handlebars" id="posts">
<button {{action 'newObject'}} ></button>
controller
App.PostsController = Ember.ArrayController.extend
actions: {
newObject: function() {
this.store.createRecord('post', {id: xxxx, title: 'Default title'});
var all = this.store.filter('post', function(post) {
return post; // return each object
}); //This filter will include the new post record
this.set('model', all); // set will notify the view of a mutated array
}
},