I am looking at using Ember.js for a new, Rails-backed, app (using Active Model Serializers). I am struggling to get my head around the framework, so maybe this is a bit of a newbie question.
My data structure is like this (simplified):
Event Days --
Events --
* Participants
* Location
Inside of an 'Event Day' there can be thousands of events (and inside an event dozens of participants and all of their data).
It seems 'wrong' that when I want to get a listing of event days I load some JSON that has the not only the EventDays but also all the Events (and then all the data from everything inside there)... basically, it loads the whole tree!
I thought I could solve this problem by using custom Serializers for the actions, but, at some point I need to get the data and Ember seems to never call the server again.
So, if I load EventDays and simply have no event data inside it Ember never calls the server to update the EventDay object when I click through to a 'show' method.
I don't know if I am being clear here. I am hoping someone who is a little ahead of me in this can understand what I am driving at!
Really I think it boils down to 2 questions:
1)How to properly filter out information on requests so that only the local objects are filled in (i.e. on a call to an index method I need a list of event days without children, but on a call to a show method I need a single event day filled with the next level down)
2) How to get Ember to 'reload' an object at the appropriate time to fill out the appropriate content
Maybe I am looking at this wrong - missing the point of something like Ember - and if so I welcome pointers to appropriate tutorials but I can't find anything (even on the Ember site) that explains how to do anything other than load the whole tree at once. With Gigs of data, this seems slow, a definite browser-killer and just plain wrong.
I appreciate my StackOverflow brethren helping me learn!
edit
As I was immediately down voted for some reason I will add code:
Client Side:
App.EventDay = DS.Model.extend({
day: DS.attr('date'),
events: DS.hasMany("Event", {async: true})
});
Server Side:
class EventDaySerializer < ActiveModel::Serializer
attributes :id, :day
has_many :events, embed: :ids, key: :events
end
edit 2
after kertap's suggestion I added the async attribute and updated the serialiser code above.
The json is here:
{"event_day":
{"id":2,
"day":"2013-12-05",
"events":[1,2,3,4,5,6,7,8]
}
}
It is worth noting that if I do not use the key: :events parameter in the serialiser things come back as "event_ids": [1,2,3,4] which, you would think is right, but causes Ember to not see the events.
Also worth noting is that if I do this:
HorseFeeder.ApplicationSerializer = DS.ActiveModelSerializer.extend({});
Then nothing works at all! I get Error while loading route: Error: Assertion Failed: The response from a findAll must be an Array, not undefined
I really don't think it should be this difficult to get the basic wiring of Ember and Rails to work...
You can tell ember data that a relationship is asynchronous.
App.EventDay = DS.Model.extend({
day: DS.attr('date'),
events: DS.hasMany("Event", {async: true})
});
If you do this and get all EventDays you will get the list of EventDays from the server side. The server side should respond with the ids of the events that are contained in an event day or you can provide a url that is a link to all events. Ember won't load the events until you need them.
Then when you call the show method for an event day and in your template you get all events for that day ember data will go off and fetch the data for events.
Related
I have an EmberJS filter like this:
/app/routes/trails/new.js
model: function (filterCurrentEmployees) {
return Ember.RSVP.hash({
trail: this.store.createRecord('trail'),
employees: this.store.query('employee', { status: '1,2'}).then(
function(data) {return data})
})
},
I was hoping that status: '1,2' would end up as a normal Rails param so I could do params[:status] and then filter the returned employees (you know so the db would get a query like 'where status IN ['1','2']')
But when this filter query is sent to Rails API I get this error:
ActiveModelSerializers::Adapter::JsonApi::Deserialization::InvalidDocument (Invalid payload ({:data=>"Expected hash"}): {"status"=>"1,2", "controller"=>"employees", "action"=>"index"}):
which occurs here in the controller:
api/app/controllers/employees.rb
def employee_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(params)
end
but that is needed for AMS to work with Ember.
So I presume something is wrong with the way I am sending/creating the query in EmberJS?
It seems to be trying to make a GET collection request. Not sure really and this explains sort-of what is going on at the Rails end, but I don't know how to get Ember to create the filter properly so that AMS is happy to accept it.
EDIT - JSON-API Spec Reference
I was under the impression that AMS and Ember 2.7 with JSON-API 'just work out of the box'. I was actually expecting the Ember filter to comply with the spec and send
/employees?filter=status[1,2]
but it seems to not be doing that. AMS says the same thing here.
UPDATE
So with some pointers from the comments (thank you) I learned that the structure of the query hash might be incorrect. I also tried changing 'query' to filter but that then raises an actual Ember error:
The filter API has been moved to a plugin. To enable store.filter using an environment flag, or to use an alternative, you can visit the ember-data-filter addon page.
Now I don't know what the difference is between an Ember query and an Ember filter, perhaps a filter only runs on the client? Who knows, the docs are so sparse and so many questions on this are 1 or 2 years old.
So after much digging around for Ember Gold Nuggets, I found out that Controllers are STILL needed for...you guessed it...Query Parameters.
I've never used a Controller, and thought I never needed one.
So it seems I was basing my attempt at using query parameters on outdated information and code examples.
For those who run into this, the gold nuggets are here and here.
Also, it seems Javascript Object parameters as per JSON-API spec are not yet supported in Ember, according to this, although it's nearly a year old so do not know if that is still true.
UPDATE
So, after further gold mining, it seems I was confused and that Controllers are needed for Query Parameters in Ember on the client side, to assist the frontend application when it transitions from one route to another (and that is when you need a Controller to set them up).
But I want to just send a query parameter from within the Route code directly to the API, and examples of doing that are very hard to find.
So in the Rails Log I would expect to see BEFORE deserialization by ASM:
Rails.logger.info "Params are: #{params.to_json}"
something like this:
Params are:
{"data":{"filter":{"status["1,2"]}},
"controller":"employees","action":"index"}
ASM expects 'data' to be the root element of the hash, and then inside that I can place my filter. For example, from the Ember Route:
model: function () {
let myFilter = {};
myFilter.data = { filter: {status: ['1,2']}};
return Ember.RSVP.hash({
trail: this.store.createRecord('trail'),
employees: this.store.query('employee', myFilter).then(function(data) {return data})
})
},
I'm just trying to use ReactRB with reactive-record.
So the deal is in render part I think. When I'm setting param :user, type: User in React Component class, I can't see any data in my table. Of course Model User in public folder, as this requirement in ReactRB.
Well, in console I see that server is fetching nothing, but right data returned.
What I'm missing? Thanks for the help!
The key for answer is in this screenshot
The details are that the data comes back from the server as a json blob
reactive-record decodes it, but counts on the fact that if you try to json parse a simple string, it raises an error.
opal 0.10 no longer raises standard error, so the whole thing just hangs up.
Just thinking about this... there is a known problem in Opal https://github.com/opal/opal/issues/1545 and this causes a problem in reactive-record. Please make sure that you are not using opal 0.10
One thing to keep in mind is that reactive-record lazy loads records, and attributes. So unless someplace in your render, you access a particular record/attribute that attribute will not show up on the client.
Its hard to tell more without a bit more of your code posted, but here is some help:
Lets say your component looks like this:
class Foo < React::Component::Base
param :user, type: User
def render
"user.name = #{user.name}"
end
end
and someplace either in a controller or in a layout you do this:
render_component '::Foo', {user: User.first}
You might try something very simple like this, just to get familiar with how things work.
What happens should be this: You will render your view and a placeholder for the first User will be sent to the component, during rendering the component looks for that user's name attribute, which it does not have, so that is queued up to fetch from the server. Rendering will complete, and eventually the data will come down from the server, the local model's data will be updated, and components displaying that data will be rerendered.
During prerendering all the above happens internal to the server, and when the component has been rendered the final html is delivered along with all the model data that was used in rendering the component. So on first load if all is working you will not see any fetches from the server.
So if you try out the above small example, and then go into your javascript console you can say things like this:
Opal.User.$first()
and you will see the underlying model data structure returned (I am translating from JS into ruby above... ruby methods all start with $)
You can then do this:
Opal.User.$first().$name()
And you can even do this (assuming there are at least 2 user models):
Opal.User.$find(2).$name()
You should have something like "DummyValue" returned, but then there will be a server fetch cycle in the console, then if you repeat the above command you will get back the actual value!!!
This may not be the best forum for more details, if you need to drop by https://gitter.im/reactrb/chat for more help
I am currently planning a complex application using ruby on rails and ember.js. What I have seen about ember-data so far is that it caches records automatically; post.comments will first result in a Ajax-call to fetch all comments for the given post, but if the user visits the same route the next time, it will just fetch the records from the store-cache.
The problem is: What if another user added a comment to this post? How to tell ember it has to reload its cache because something changed?
I already thought about a solution using websockets to tell clients which stuff to reload - but I don't think this is best-practice. And in addition, I can't imagine this isn't a common problem, so I am wondering what other developers are doing to solve this issue.
I tried to implement model updating in (experimental) chat application. I have used SSE: ActionController::Live on server side (Ruby on Rails) and EventSource on client side.
Simplified code:
App.MessagesRoute = Ember.Route.extend({
activate: function() {
if (! this.eventSource) {
this.eventSource = new EventSource('/messages/events');
var self = this;
this.eventSource.addEventListener('message', function(e) {
var data = $.parseJSON(e.data);
if (data.id != self.controllerFor('messages').get('savedId')) {
self.store.createRecord('message', data);
}
});
}
}
});
App.MessagesController = Ember.ArrayController.extend({
actions: {
create: function() {
var data = this.getProperties('body');
var message = this.store.createRecord('message', data);
var self = this;
message.save().then(function (response) {
self.set('savedId', response.id);
});
}
}
});
The logic is simple: I'm getting each new record from EventSource. Then, if record was created by another client, the application detects it and new record being added to store using ember-data's createRecord. Suppose this logic may have some caveats, but at least it serves well as 'proof of concept'. Chat is working.
Full sources available here: https://github.com/denispeplin/ember-chat/
I have something to say about reloading: you probably don't want to perform full reloading, it's resource-consuming operation. Still, your client side needs some way to know about new records. So, getting new records one-by-one via SSE is probably the best option.
If you just want to get rid of caching you can force a reload every time user navigates to comments route. But this largely depends on what you are trying to acheieve, I hope comments is just an example.
If you want your ui to get updated automagically with changes in server, you need some communication with server, some polling mechanism like websocket or polling from a webworker. Then you may reload the list of changed records sent from server. You are probably on the right track with this.
You can as well take a look at the orbitjs standalone library that integrates well with Ember. This is more useful if you require local storage as well and got to manage the multiple data sources.
This is really a common problem with any web application, no matter what framework you are using. From my point of view, there are two main options. One: You have a service that polls the server to check to see if there are any changes that would require you to reload some of your models, have that service return those model IDs and refresh them. The other option is as you suggested, using a websocket and pushing notifications of model changes/new models themselves.
I would opt to actually just send the comment model itself, and push it into the Ember store and the associated post object. This would reduce the need to hit the server with a hard refresh of your model. I am currently using this method with my Ember app, where there is an object that contains overview data based on all the models in my app, and when a change is made in the backend, my websocket server pushes the new overview data to me application.
UPDATE:: I meant for this to be a comment, not an answer, oh well.
I've had this same issue with mobile app development. While websockets seemed like the first answer, I was worried about scalability issues with limited server resources. I decided to stick with the Ajax call to fetch newly modified records. This way server resources are only used if the user is active. However, as others pointed out, returning all comments every single time you need data makes your cacheing useless and is slow. I suggest updating your rails server to accept an optional timestamp. If the timestamp is not supplied, then every comment is retrieved. If a timestamp is supplied, then only return comments where the updated_at column is >= the supplied timestamp. This way, if no comments were added or modified since your last call, then you quickly get back an empty list and can move on. If results are returned, you can then merge them with your existing list and show the updated comments.
Example of fetching newly created or modified comments
if params.has_key?(:updated_since)
comments = Post.find(params[:id]).comments.where("updated_at >= ?", params[:updated_since])
else
comments = Post.find(params[:id]).comments
end
I am attempting to get all the orders from a magento instance. Once a day we grab all the orders.. (sometimes a few thousand)
Extra stuff that's more why I ask:
I'm using ruby-on-rails to grab the orders. This involves sending the soap call to the magento instance. It's easy as.
Once I have the response, I convert it into a Hash (a tree) and then pick out the increment id's of the orders and proceed to call getOrder with the increment id.
I have two problems with what's going on now, one operational, and one religious.
Grabbing the XML response to the list request takes really really long and when you tack on the work involved in converting the XML to a hash, I'm seeing a really slow processes.
The religious bit is that I just want the increment_ids so why do I have to pay for the processing/bandwidth to support a hugely bloated response.
Ok so the question...
Is there a way to set the response returned from Magento, to include only specific fields? Only the updated_at and the increment_id for instance.
If not, is there another call I'm not aware of, that can get just the increment_ids and date?
Edit
Below is an example of what I'm looking for from magento but it's for ebay. I send this xml up to ebay, and get back a really really specific bit of info about the product. It works for orders and such too. I can say "only this" and get just that. I want the same from Magento
<GetItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
<SKU>b123-332</SKU><OutputSelector>ItemId</OutputSelector>
</GetItemRequest>
I've created a rubygem that gives you your salesOrderList response in the form of a hash, and you can do what you want with the orders after you've received them back (i.e. select the fields you want including increment_id). Just run
gem install magento_api_wrapper
To do what you want to do, you would do something like this:
api = MagentoApiWrapper::Sales.new(magento_url: "yourmagentostore.com/index.php", magento_username: "soap_api_username", magento_api_key: "userkey123")
orders = api.order_list(simple_filters: [{key: "status" value: "complete"}])
orders.map {|o| [o.increment_id, o.items.first.sku] }
Rough guess, but you get the idea. You would get the array of hashes back and you can do what you want with them after that. Good luck!
I'm writing an application that requires 3 inputs a day for every day of the year. (3*365=1095). I'm struggling with a way to output each field in an efficient manner.
The inputs are not all-or-nothing (you could fill in 10 days worth of input, hit save, and come back later to fill in more)
I attempted to do this by building all 1095 objects in the controller and then outputting the inputs in the view, but obviously this is really slow and probably memory intensive.
Any suggestions? I'm leaning toward writing the entire form client-side and then filling in the existing elements using AJAX.
EDIT
The model is called Timing and has these attributes:
month, day, time1, time2, time3
so there are 365 models to be saved.
Sounds like you've got a nested resource. You have a resource called timing which contains a resource called, what, day?
#routes
resources :timing do
resources :day
end
So assuming that when timing is created, you have all 365 days created as well (sounds like a pretty expensive operation). Displaying the fields isn't that tricky. You could just do
#controller
def show
#timings = Timing.all
end
#view
(Date.beginning_of_year..Date.end_of_year).each do |day|
t = #timings.find { |timing| timing.date == day } #or some other method of deciding that the current day has a timing
unless t.nil?
form_for t #etc
else
form_for Timing.new #etc
end
end
Then perhaps you could make each for submit via UJS and call it a day. Though this sounds like a pretty complicated design, but I'm not sure what your problem area is.
If I understand your question correctly, you want a way to dynamically show time inputs, 3 of them per day, on a form.
If the above is correct, what I would suggest is that you do the nested resource as #DVG has detailed, and load the current day only. If you need to load multiple days, you can easily request that through UJS (Ajax) and load it on the same page.
What you probably want to do, in order not to melt down the server, is auto-save the time inputs or auto-save each day's time inputs when the grouping loses focus.
#DVG's answer probably works fine, but it keeps all of the work on the server.
What I ended up going with was my initial thought: get all of the existing timings like this:
def edit
#timings = Timing.find_by_store_id(params[:store_id])
end
then in the view, I wrote two javascript functions: one that writes all 365 rows with all 3 columns. Once the field were all output in Javascript, I used another function that took the existing records and inserted them into the form:
<script type="text/javascript">
function updateForm(){
timings = <%= #timings.to_json %>;
... fill out the fields ...
}
</script>
It works nice and fast, and best of all, no AJAX calls. Of course one caveat is that this fails if the user has Javascript disabled, but that's not an issue for me.