I have an existing search app I built on the server-side that uses elasticsearch processed by python/flask. On the client side, it's using JS/jQuery and handlebars to parse and present the data.
I'd like to take it one step further and build it in ember, as the "controls" (filters, sorting, pagination, etc) make it a perfect candidate for an SPA.
I understand the ember fundamentals, but I feel like I've hit a wall with ember-data - i.e. how to get my data into the store so I can bind actions. Can somebody point me in the right direction?
My existing JSON API is accessed like this:
http://localhost:8000/search?q=shirt&paging=18&filter-price=100
and it returns JSON like this:
{
"aggregations": {
"breadcrumb": [],
"color": [],
"price": [],
"size_apparel": [],
"size_jewelry": []
},
"meta": {
"agg_list": [],
"default_sort": "",
"key_translations": {},
"paging": 18,
"paging_options": [],
"sort_methods": [],
"from": 0,
"hits": 89,
"pages": 5,
"q": "shirt"
},
"results": [
{
"creation_date": "",
"image": "",
"images": {
"altimg1": "",
"large": "",
"regular": "/",
"small": ""
},
"name": "This Product",
"price": "19.95",
"skuid": "ABC123",
"url": "shirt.html"
},
{...},
{...}
]
}
Is this usable or do I need to rewrite the back-end?? I've tinkered with accessing the data and have something very rough working. I can actually see the 'results' data, but I have NO IDEA how to access the 'meta' and 'aggregations' data. I think this is "multiple models in the sam payload" and RSVP.hash should work...but ember-data seems to have a requirement that each model have an id. That makes sense for the "results" model, but not for aggregations and definitely not for meta.
For now I just want to get it to show up.
For starters, my adapter is:
export default DS.RESTAdapter.extend({
host: 'http://localhost:8000',
pathForType() {
return 'search';
}
});
Test controller is:
export default Ember.Controller.extend({
queryParams: ['q','paging'],
q: 'shirt',
paging: '18'
});
my 'result' model is:
const { attr } = DS;
export default Model.extend({
'creation_date': attr('string'),
'has_pricerange': attr('string'),
'image': attr('string'),
'name': attr('string'),
'price': attr('string'),
'skuid': attr('string'),
'url': attr('string')
});
route:
export default Ember.Route.extend({
model(params) {
return this.store.query('result', {
q: params.q,
paging: params.paging
});
}
});
serializer:
export default DS.RESTSerializer.extend({
primaryKey: 'skuid'
});
This is so easy to do with jquery - you just $.get and then access the data with object notation. I know ember is not jquery, but it seems like it should be easier to access data. Am I missing something? Taking the wrong approach? Should I start from scratch?
Ember Data is a great piece of the Ember puzzle, but by no means required. Whether or not you should use Ember Data really depends on the nature of your data, the control of your API, and your desired futureproofability
Nature of your Data
Ember Data really excels at your typical model/entity type data. "Things" that have attributes, and can relate to each other. If your data follows that pattern then Ember Data could be a great fit.
Control of your API
As mentioned in the other answer, you get a lot for free if you can buy into a JSON API format. This has become the standard for Ember Data, but is not exclusive to Ember. A lot of server-side options will have ways to serialize these resources easily with many existing frameworks. If you can't change the data coming from the server, or if it's easier to just handle it on the front-end, you can go down the path of customizing the adapters/serializers in Ember.
Desired Futureproofability
Ember Data allows you to swap out the adapter/serializer while keeping your models in tact. This is desirable if you want your application to handle different sources in the future, maybe swap from using your own API to using a local storage, or a 3rd-party service like firebase. If don't plan on much changing, you can do your basic ajax calls and return a promise within your model hook and Ember will work more or less the same. I would recommend using ember-network which is spec'ed against the fetch API and will likely become more and more supported as a FastBoot compatible request.
Other things to consider is that Ember Data and using the Ember Object can be heavy depending on the amount of instances of each model you'll be passing in.
Others are tackling this problem, too. Toran Billups has pulled the redux pattern with ember-redux, which is another great way of thinking about how you approach your data.
Your use case
So you could:
Use Ember Data with find
Taking a look at the shape of your data, it looks like you're providing more of a search service than querying for specific models/entities. Ember Data does have a find method, but the amount of metadata you're providing it might be overloaded for the model use case.
Forget Ember Data and use search end point
model: function(params), Use the params from the model hook to construct the url to the search end point, and return a promise (add then as needed to shape the data)
Query the search end point (API refactor)
Similar idea as above, but you would use get the product id's and use those to query the ember data store for the products.
Something similar to:
fetch("service-end-point?with=queryparams")
.then(result => {
return {
products: this.store.findMany(Product, result.ids);
};
});
I think it'd be easiest to get started just handling and shaping the data directly in the controller, and skipping Ember Data. If you want specific computed property bindings there's no reason why you can extend Ember.Object with the shape you want and then use something like .map to take the result from the network request and apply it like payload => Object.create(payload).
Let me know if you have any questions surrounding any of these ideas.
I recommend you read this section of the guide, specifically the bit about $.getJSON. Ember is designed to use a powerful store. It's much easier to use if you're using a standard api (e.g. JSON API); you can just use Ember data code directly. But if not, you will have to write some serializer code so Ember knows how to use your api. Once that's done though, your component code won't be coupled to your data fetching code.
You can use $.getJSON if you are still prototyping, though.
Related
I am trying to learn Ember by developing a simple TODO manager application.
I am using ember-data and JSONAPISerializer with a hand rolled REST JSON API backend for this application.
I have the following model which represents a task
app/model/task.js
export default DS.Model.extend({
title: DS.attr ('string'),
description: DS.attr ('string'),
isComplete: DS.attr ('boolean')
});
The corresponding JSON data from backend looks like this
{
"data": [{
"id": "1",
"type": "task",
"attributes": {
"title": "Complete Ember TODO manager application",
"description": "Build a simple Ember application for easily managing tasks",
"is-complete": "false"
}
}]
}
As per the convention, the Ember model uses camel cased names and the JSON API backend uses dasherized names.
A basic feature in this application is to filter tasks by their status, so basically one can see either ongoing or completed or all tasks.
For fetching only the ongoing tasks, I have the following query in the model hook of the corresponding route
app/routes/tasks/ongoing.js
export default Ember.Route.extend({
model () {
return this.get('store').query('task', {
filter: {
isComplete: 'false'
}
});
}
});
So when the query is sent to the backend it goes as
restjsonapi/tasks?filter[isComplete]=false
The problem is that the backend expects "is-completed" and does not understand "isComplete".
Is there a way to dasherize the URL emitted by Ember query?
Edit (possible workaround):
I might have been trying to solve this the wrong way I think. I have changed the JSON data to have underscores instead of dashes and worked around this problem.
When using store.query, Ember then finds the relevant adapter for your model type and calls adapter.query which in turn seems to pass your query to jQuery.ajax as a value for the data attribute. See here:
https://github.com/emberjs/data/blob/v2.7.0/addon/adapters/rest.js#L504
The conversion from Object to Query String is then done by jQuery using encodeURIComponent as you can see there: https://github.com/jquery/jquery/blob/e4fd41f8fa4190fbbb6cb98cf7ace64f6e00685d/src/serialize.js#L68
So you won't be able to change this behaviour by passing an option to Ember.
However, you could try something like:
export default Ember.Route.extend({
model () {
return this.get('store').query('task', {
filter: {
"is-complete": 'false'
}
});
}
});
And it should work.
So let's say you have a User model which is set up to a fairly standard API. On the front end you have an Ember project which also has that User model. The normal creation call would be something like the following:
store.createRecord('user', {email: 'test#gmail.com'}).save();
This would send off a POST request to something like /api/users. However, something fairly extensive API's support is the creation of multiple models at once. So for instance, instead of the POST call just sending a single object under user: {email: 'test#gmail.com'} it would send an array of objects like users: [{email: 'test#gmail.com'}, {email: 'test2#gmail.com'}, ...].
How I have seen this handled in ember is to just do multiple creation calls at runtime. However, this is terribly inefficient and I am wondering if Ember supports saving multiple models at the same time? How would you achieve this in Ember?
You cannot save an array of models in a single POST request Ember Data as you describe it, however there is a way.
You can save a parent model which hasMany 'user' with the EmbeddedRecordsMixin, which will include either relationship ids or full records. Your serializer would look like -
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
users: { embedded: 'always' },
}
});
Depending on your use case it may make sense to create a parent model only for this purpose which hasMany 'user'. If you want to use an existing model and don't always want to embed its user records there is an answer here.
If you do decide to save the models individually, you would want to do users.invoke('save'), which will trigger a POST for each model.
If you're asking specifically about Ember Data, I don't know of any way of doing that (I don't think it's possible to use any equivalent of save() on a collection/array). There could be alternative Data libraries that may work (for instance you could check Orbit.JS - which is something I haven't done yet)
The way I've done it it to have a custom endpoint on my backend that receives a certain JSON payload and creates the resources. You do it by issuing a regular ajax call, see this example (from a project of mine).
let content = //get content that you want to post
let accessToken = this.get('session.session.authenticated.token');
Ember.$.ajax({
data: JSON.stringify(content),
dataType: 'json',
method: 'POST',
url: 'path/to/my/custom/end/point',
headers: {
'Content-Type': 'application/json',
'Authorization': `Beader ${accessToken}`
}
}).then((result) => {
// Code for success
}, (jqXHR) => {
// Code for error
}).always(() => {
// Code for always/finally
});
As you can see this is all custom code, not leveraging Ember Data store or models. So far I haven't found a better answer.
EDIT: After seeing andorov's answer. I forgot to mention something. I'm using Ember Data 2.0 (JSONAPI by default) and EmbeddedRecordsMixin does not work property with JSON API
Ember : 1.13.3
Ember Data : 1.13.5
jQuery : 1.11.3
I am trying to send a JSON payload using ember-data from my EmberJS client to my server. I want to send the entire object graph to the server on saving the project, as I don't want to send multiple requests. I wouldn't mind sending multiple requests, but I am worried about what happens if one of the requests fails in the middle and the data on the server will not be correct.
I wanted to use JSONAPI (http://jsonapi.org/format/#document-compound-documents) as that is becoming the default adapter in Ember. Also, there is a few C# libraries that handle this format, so I thought it would be quite straightforward. However, after reading the spec, it seems that I cannot embed objects if they do not have an id. EmberJS also does not attach the child objects to the JSON either even though I have specified { async: false, embedded: 'always' }) on the DS.attr.
My question is: If an application is used in such a way that an object graph is created on the client side, how do you use JSONAPI format to send the entire object graph to the server? Do I have to generate ids on the client side to satisfy the JSONAPI standard? Then once they get to the server just ignore them so they get saved with an id generated by the ORM?
Here is my labelGroup model:
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
labels: DS.hasMany('label-model', { async: false, embedded: 'always' })
});
Here is my project model:
import DS from 'ember-data';
export default DS.Model.extend(DS.EmbeddedRecordsMixin, {
name: DS.attr('string'),
labelGroups: DS.hasMany('labelGroup', { async: false, embedded: 'always'})
});
Here is the POST that I get after doing a save() on the project:
{
"data":{
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null},
{"type":"label-groups","id":null}
]
}
},
"type":"label-projects"
}
}
UPDATE: I tried using https://www.npmjs.com/package/ember-cli-uuid to generate client side ids which it has. However the data getting output does not include the extra objects, only a reference to their ids. I expected to see an "included" property as specified here:http://jsonapi.org/format/#document-compound-documents, but it is not there.
{
"data":{
"id":"7b4544ee-91cd-493d-8b10-52040e68c283",
"attributes":{"name":"Project"},
"relationships":{
"label-groups":{
"data":[
{"type":"label-groups","id":"08115273-e82a-4d46-93ea-232ce071fb78"},
{"type":"label-groups","id":"9ca94fe9-8077-411e-98d2-1694c6fecce4"},
{"type":"label-groups","id":"d629f1e8-7962-404d-8034-38229ab21f77"},
{"type":"label-groups","id":"c6bda655-5489-4760-847b-bf02239bb2c5"},
{"type":"label-groups","id":"f6fef249-2d1d-43f0-ba64-24b7ff8b5637"},
{"type":"label-groups","id":"a7db25bf-52c8-477b-83e4-64e7c76b072e"},
{"type":"label-groups","id":"f3b5fbb3-261a-4b3d-b481-b9352f8ce2d6"}
]
}
},
"type":"label-projects"
}
}
Ember-data has no support for what you want at the moment. So ember-data will not save your relationships data in a save payload.
But its possible to do this your own by using a custom adapter and serializer.
I strongly recommend you to checkout the API and then look into the source.
If you call .save() on your Model the createRecord method is called on your adapter.
Here serializeIntoHash on the serializer is called to serialize the Model.
serializeIntoHash calls serialize, where serializeBelongsTo and serializeHasMany is called.
Now you can just override serializeHasMany and modify the hasMany before the line:
json[payloadKey] = hasMany;
Here you have the type and the ids as they are sent by ember-data. You could just .forEach the data on the hasMany and then fetch the store for the data and build your included array.
I hope this helps you to understand the serializer and the adapter so you can modify it to do whatever you want pretty easy. Actually this is the best part about ember-data. The structure of the adapter and the serializer, which allows easy modifications.
Where should I define fixtures in an Ember JS app generated with ember-cli? I've tried numerous places, such as app.js and within a folder called "fixtures".
After digging around I discovered that changing Ember.MODEL_FACTORY_INJECTIONS = true; in the file app.js to Ember.MODEL_FACTORY_INJECTIONS = false; is solving the problem.
Through this question I also found another solution where you don't have to change the configuration:
Instead of defining the fixtures as described you have to use reopenClass:
//models/item.js
var Item = DS.Model.extend({...});
Item.reopenClass({
FIXTURES: [
{ id: 1, ... },
{ id: 2, ... }
]
});
export default Item
Happy developing with Ember and ember-cli :-)
Instead of using fixtures, the way I'm doing it in Ember CLI 0.0.40 is generating api stubs.
ember generate api-stub tasks
I'm a node.js beginner, but from the looks of it, it sets up an express server script to respond to the /tasks endpoint, corresponding to the name you pass to the command, in the format the Ember REST adapter is expecting. Then you simply fill in the empty array with your fixture data. Easy peesy!
The benefit I see is that I won't have to rework anything later to integrate with a restful api getting me one step closer to launching a real app some day.
This generator is not thoroughly documented yet. It's only displayed as an item in the ember help generate command, which I was brave/desperate/curious/stupid enough to try.
If you use findQuery to get your data you will get this error when using the method above:
Error while loading route: Error: Assertion Failed: Not implemented: You must override the DS.FixtureAdapter::queryFixtures method to support querying the fixture store.
To fix this I created an adapter for the model and implemented the queryFixtures method to return the fixtures.
#/adapters/[model-name].js
queryFixtures: function() {
return [ {
"key" : "value",
},
{
"key" : "value",
},
etc...
]
};
I had to do this in addition to reopenClass in my model definition as stated above. In fact it was the same data, I cut and pasted. This smells a little bad, but it works. I'm sure there's a better way to do this without duplicating the fixtures, I just don't know what it is.
I define it in the model folder
//models/item.js
var Item = DS.Model.extend({...})
Item.FIXTURES = [
{
id:1,
...
}
];
export default Item
I have the following user object (json):
{
"_id": "my-id",
"org": 666,
"type": "user",
"properties": {
"first_name": "Firstname",
"surname1": "Surname1",
"surname2": "Surname2",
"allowed_apps": [ "one-app", "another-app" ],
"default_app": "one-app",
"email": "email#address.com"
},
"outputs": {
"extension": "extension-id"
}
}
This is a single model, with a complex structure. It is not a "multi-model structure". Therefore, I want to define this using DS.Model.extend, but I do not want to use belongsTo relationships for neither properties nor outputs, since they do not refer to other models: these fields are just complex data, direct part of my user object. That is, we do not have any properties model, nor any outputs model. These are just parts of the user model, and as such are stored in the database (couchdb in our case).
Can this be done in ember?
Lamentably this kind of structure is not possible using ember-data models.
Every value of a model that is not a primitive one such as string, number, boolean & date can't be defined as a model property without designing it with belongsTo or hasMany. Furthermore this site jsonapi.org which is still a WIP describes the direction ember-data is going with it's implementation.
So the point is here if you want/need to use ember-data models (DS.Model) your server should obey the JSON format ember-data expects, otherwise you have always the possibility (since ember-data is backend agnostic) to not use ember-data models definition at all, this way your models can be structured the way you want, but then you are out of being helped from the conventional work ember-data adapter and serializer does for you and you have to write your own adapter/s which then deals with all the peculiarities your JSON's have and load them finally into your store.
If you absolutely need a custom data structure to be exchanged with your backend, you can register a custom transform like for example:
App.Adapter.registerTransform('mega', {
deserialize: function(serialized) {
//do your custom deserialization
},
serialize: function(deserialized) {
//do your custom serialization
}
});
and then use it like:
App.MyModel = DS.Model.extend({
megaValue: DS.attr('mega');
});
Hope it helps.