I got live updates working with ember and Rails 4 with the help of Railscast #401. ActionController::Live on the backend and EventSource in my Ember code.
The event listener prepends a div with the content sent from the server, but there are 2 problems:
The template on my local browser updates automatically, causing 2 duplicate records to display. So I tried to create a temporary client ID and compare ids before prepending to the DOM. This proved to be quite glitchy, plus it doesn't seem like the ember way...
I found the ember store 'push' and 'pushPayload' methods but I couldn't get those to update my template either.
Here is the relevant code:
Method 1 Using DOM prepend -
Auth.NotebookIndexRoute = Ember.Route.extend(
model: ->
#modelFor('notebook').get('notes')
activate: ->
self = #
source = new EventSource('/api/v1/testposts')
source.addEventListener 'message', (e) ->
data = $.parseJSON(e.data)
unless self.controllerFor('postsNew').get('savedId') is data.id
$("#allposts").prepend $("<div class=\"post\">").text(data.content)
'savedId' is set in the Posts.new controller after a post is saved. This is hit or miss...
Method 2 Using store.push -
Auth.NotebookIndexRoute = Ember.Route.extend(
model: ->
#modelFor('notebook').get('notes')
activate: ->
self = #
source = new EventSource('/api/v1/testposts')
source.addEventListener 'message', (e) ->
data = $.parseJSON(e.data)
self.store.push "post",
id: data.id
content: data.content
The push method does not update the template.
Method 3 - Works SOME of the time
Auth.NotebookIndexRoute = Ember.Route.extend(
model: ->
#modelFor('notebook').get('notes')
activate: ->
self = #
source = new EventSource('/api/v1/testposts')
source.addEventListener 'message', (e) ->
data = $.parseJSON(e.data)
self.store.find("post", data.id).then (stuff) ->
console.log('PUSH NEW POST')
posts = self.modelFor('notebook').get('posts')
posts.addObject(stuff)
When I open up a Chrome and Firefox browser side by side and add new posts, they'll show up only about 60-70% of the time...still looking for where my error might be.
Thanks in advance for any help I can get.
Using Ember-Data, by adding the object that is sent by the server, the application is able to sync to the server and update the template. This seems clean and it works consistently...but only in Chrome, not in Firefox.
I'm adding it as an answer because it's a step forward for this question, but its not the correct answer yet. Hoping to find something soon.
Auth.NotebookIndexRoute = Ember.Route.extend(
activate: ->
self = #
source = new EventSource('/api/v1/testposts')
source.onmessage = (e) ->
console.log('This shows up in Chrome but not in Firefox')
data = $.parseJSON(e.data)
self.store.find("post", data.id).then (stuff) ->
posts = self.modelFor('notebook').get('posts')
posts.addObject(stuff)
stuff.save()
I am attempting to create a new record using Ember Data (the latest beta release, number 6). I seem to have most things working but creating and persisting a new model is not setting the attributes returned from the server on the model (including the id).
Here's the code I have in my controller.
actions:
cancel: ->
#transitionToRoute('brews.index')
save: (brew) ->
self = this
brew.save().then( ->
console.log "Brew id is #{brew.get('id')}"
console.log "Brew effciency is #{brew.get('efficiency')}"
self.transitionToRoute('recipe', brew)
)
The output console logging is returning null for the ID and undefined for the other attribute.
I believe my json is being returned properly from the server:
{"brew":{"id":57,"name":"Yadda Yadda","created_at":"2014-02-16T23:44:46Z","efficiency":75}}
I'm using the DS.ActiveModelAdapter. Any ideas why this ember isn't seeing the attributes returned from the server?
Have you tried using an argument with your promise function?
This way in that function scope it can access the saved instance returned by the promise, with the id available.
actions:
cancel: ->
#transitionToRoute('brews.index')
save: (brew) ->
self = this
brew.save().then( (b)->
console.log "Brew id is #{b.get('id')}"
console.log "Brew effciency is #{b.get('efficiency')}"
self.transitionToRoute('recipe', b)
)
I am attempting to test that calling the move action will cause a modal view's controller to setup with the proper model.
asyncTest("attempting to move directories will setup the folder_tree_controller's model", 1, ->
User.create({email: 'user#email.com', session: 'session_token', card: Cards.FIXTURES[0].id})
cardController = App.__container__.lookup('controller:card')
Em.run -> cardController.set('model', null)
Em.run -> controller.send('move')
wait()
ok(cardController.get('model'))
start()
)
Controller gist:
Controller = Ember.Controller.extend({
actions: {
move: ->
self = #
#get('store').find('card', User.current().directory).then (card) ->
self.send('showMoveDialog', card)
false
}
})
However during the test execution I error out and receive the following message:
Error: Can't trigger action 'showMoveDialog' because your app hasn't finished transitioning into its first route. To trigger an action on destination routes during a transition, you can call `.send()` on the `Transition` object passed to the `model/beforeModel/afterModel` hooks.
Source:
at Test.QUnitAdapter.Test.Adapter.extend.exception (http://localhost:8000/vendor/ember/index.js:40219:5)
at superWrapper [as exception] (http://localhost:8000/vendor/ember/index.js:1230:16)
at Ember.RSVP.onerrorDefault (http://localhost:8000/vendor/ember/index.js:16520:28)
at Object.__exports__.default.trigger (http://localhost:8000/vendor/ember/index.js:8399:13)
at Promise._onerror (http://localhost:8000/vendor/ember/index.js:9123:16)
at Promise.publishRejection (http://localhost:8000/vendor/ember/index.js:9530:17)
at Object.DeferredActionQueues.flush (http://localhost:8000/vendor/ember/index.js:5654:24)
at Object.Backburner.end (http://localhost:8000/vendor/ember/index.js:5745:27)
Am I missing something while attempting to test setting up the modal view?
Set up your controller like this and it will be able to call send in your tests:
cardController = App.CardController.create({
container: App.__container__
});
This as opposed to doing the container.lookup(...)
When I define a controller action to display dates occuring a particular date, it works correctly, but If I convert that controller action to a property it stops displaying the date occuring on a particular event. The jsfiddle
App.EventsController = Em.ArrayController.extend({
todayEvent: function(date){
return this.get('content').filter(function(event) {
return (moment(event.get('start')).unix() == moment(date).unix());
});
}
});
I can fetch an instance of the controller:
u = App.__container__.lookup("controller:events")
on the event 25th, there are 2 events and I can fetch it with
u.todayEvent(new Date('2013-07-25').toString())
which correctly returns
[> Class, > class]
But in the CalendarEvent controller, I want to display the events for a particular date just like above but this time using computed-property, so I redefine todayEvent asa computed property as shown below, only this time, it only returns true or false instead returning class objects containg the events for that day.
The date property is set using controllerFor in the router serializers hook instead of passing it in as we did when we defined todayEvent as a controller action previously.
App.CalendarEventController = Em.ObjectController.extend({
date: null,
needs: ['appointments'],
todayEvent: function(){
var _self = this;
var appoint = _self.get('controllers.appointments');
var appCont = appoint.get('content');
return appCont.map(function(appointee) {
return (moment(appointee.get('event.start')).unix() == moment(_self.get('date')).unix());
});
}.property('date')
});
Now I click on the link for appointment, then the link for calendar and then click one of the dates in red from the calendar, so the serializer hook can set the controller date and I then go into the console:
u = App.__container__.lookup("controller:calendarEvent")
try to fetch the events occuring on that date in the console with:
u.get('todayEvent')
I either get an empty array like this [ ] or if I filter using map() instead of filter(), then it returns [false, false, false]
The jsfiddle
It looks like you need to add 'content.#each' to your computed property.
As it stands now 'todayEvent' will only be computed when 'date' changes I am guessing date is being set before or at the same time as the content.
todayEvent is returning [false, false] because you are using map not filter.
todayEvent: function(){
var _self = this;
var appoint = _self.get('controllers.appointments');
var appCont = appoint.get('content');
return appCont.filter(function(appointee) {
return (moment(appointee.get('event.start')).unix() == moment(_self.get('date')).unix());
});
}.property('content.#each', 'date')
ember-data.js: https://github.com/emberjs/data/tree/0396411e39df96c8506de3182c81414c1d0eb981
In short, when there is an error, I want to display error messages in the view, and then the user can 1) cancel, which will rollback the transaction 2) correct the input errors and successfully commit the transaction, passing the validations on the server.
Below is a code snippet from the source. It doesn't include an error callback.
updateRecord: function(store, type, record) {
var id = get(record, 'id');
var root = this.rootForType(type);
var data = {};
data[root] = this.toJSON(record);
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
this.didUpdateRecord(store, type, record, json);
}
});
},
Overall, what is the flow of receiving an error from the server and updating the view? It seems that an error callback should put the model in an isError state, and then the view can display the appropriate messages. Also, the transaction should stay dirty. That way, the transaction can use rollback.
It seems that using store.recordWasInvalid is going in the right direction, though.
This weekend I was trying to figure the same thing out. Going off what Luke said, I took a closer look at the ember-data source for the latest commit (Dec 11).
TLDR; to handle ember-data update/create errors, simply define becameError() and becameInvalid(errors) on your DS.Model instance. The cascade triggered by the RESTadapter's AJAX error callback will eventually call these functions you define.
Example:
App.Post = DS.Model.extend
title: DS.attr "string"
body: DS.attr "string"
becameError: ->
# handle error case here
alert 'there was an error!'
becameInvalid: (errors) ->
# record was invalid
alert "Record was invalid because: #{errors}"
Here's the full walk through the source:
In the REST adapter, the AJAX callback error function is given here:
this.ajax(this.buildURL(root, id), "PUT", {
data: data,
context: this,
success: function(json) {
Ember.run(this, function(){
this.didUpdateRecord(store, type, record, json);
});
},
error: function(xhr) {
this.didError(store, type, record, xhr);
}
});
didError is defined here and it in turn calls the store's recordWasInvalid or recordWasError depending on the response:
didError: function(store, type, record, xhr) {
if (xhr.status === 422) {
var data = JSON.parse(xhr.responseText);
store.recordWasInvalid(record, data['errors']);
} else {
store.recordWasError(record);
}
},
In turn, store.recordWasInvalid and store.recordWasError (defined here) call the record (a DS.Model)'s handlers. In the invalid case, it passes along error messages from the adapter as an argument.
recordWasInvalid: function(record, errors) {
record.adapterDidInvalidate(errors);
},
recordWasError: function(record) {
record.adapterDidError();
},
DS.Model.adapterDidInvalidate and adapterDidError (defined here) simply send('becameInvalid', errors) or send('becameError') which finally leads us to the handlers here:
didLoad: Ember.K,
didUpdate: Ember.K,
didCreate: Ember.K,
didDelete: Ember.K,
becameInvalid: Ember.K,
becameError: Ember.K,
(Ember.K is just a dummy function for returning this. See here)
So, the conclusion is, you simply need to define functions for becameInvalid and becameError on your model to handle these cases.
Hope this helps someone else; the docs certainly don't reflect this right now.
DS.RESTAdapter just got a bit more error handling in this commit but we are still not yet at a point where we have a great recommendation for error handling.
If you are ambitious/crazy enough to put apps in production today with ember-data (as I have been!), it is best to make sure that the likelihood of failures in your API is extremely low. i.e. validate your data client-side.
Hopefully, we can update this question with a much better answer in the coming months.
I just ran into such a situation, not sure if this is already explained anywhere.
I am using:
Em.VERSION : 1.0.0
DS.VERSION : "1.0.0-beta.6"
Ember Validations (dockyard) : Version: 1.0.0.beta.1
Ember I18n
The model was initially mixedin with Validation mixin.
App.Order = DS.Model.extend(Ember.Validations.Mixin, {
.....
someAttribute : DS.attr('string'),
/* Client side input validation with ember-validations */
validations : {
someAttribute : {
presence : {
message : Ember.I18n.t('translations.someAttributeInputError')
}
}
}
});
In the template, corresponding handlebars is added. (note that ember validations will automatically add errors to model.errors.<attribute> in case of input validations, I will be using same trade-off in server validations as well)
<p>{{t 'translations.myString'}}<br>
{{view Ember.TextField valueBinding="attributeName"}}
{{#if model.errors.attributeName.length}}<small class="error">{{model.errors.attributeName}}</small>{{/if}}
</p
Now, we will be saving the Order
App.get('order').save().then(function () {
//move to next state?
}, function(xhr){
var errors = xhr.responseJSON.errors;
for(var error in errors){ //this loop is for I18n
errors[error] = Ember.I18n.t(errors[error]);
}
controller.get('model').set('errors', errors); //this will overwrite current errors if any
});
Now if there is some validation error thrown from server, the returned packet being used is
{"errors":{"attributeName1":"translations.attributeNameEror",
"another":"translations.anotherError"}}
status : 422
It is important to use status 422
So this way, your attribute(s) can be validated client side and again on server side.
Disclaimer : I am not sure if this is the best way!
Since there's currently no good solution in stock Ember-Data, I made my own solution by adding an apiErrors property to DS.Model and then in my RestAdapter subclass (I already needed my own) I added error callbacks to the Ajax calls for createRecord and updateRecord that save the errors and put the model in the "invalid" state, which is supposed to mean client-side or server-side validations failed.
Here's the code snippets:
This can go in application.js or some other top-level file:
DS.Model.reopen({
// Added for better error handling on create/update
apiErrors: null
});
This goes in the error callbacks for createRecord and updateRecord in a RestAdapter subclass:
error: function(xhr, textStatus, err) {
console.log(xhr.responseText);
errors = null;
try {
errors = JSON.parse(xhr.responseText).errors;
} catch(e){} //ignore parse error
if(errors) {
record.set('apiErrors',errors);
}
record.send('becameInvalid');
}