I'm writing an ember application that pulls the majority of it's data from the Lastfm API. The API is not RESTful. I'm not sure what what level of abstraction I should customize. Should I go down the path of writing a custom LastFm ember-data adapter? Or should I just sidestep ember-data all together?
They return data similar to this:
{ "recenttracks" : { "meta" : {}, "tracks" : [ { track info }, { track info } ] } }
For requesting data, they have a scheme that involves sending a method parameter. So, not the worst thing ever, but certainly not RESTful.
Anyway, just looking for a bit of direction as I'm new to ember-data.
Thanks!
Personally, I would create a new adapter, not necessarily RESTAdapter, passing parameters to find and findAll:
var lastFmAdapter = DS.Adapter.create({
find: function (store, type, id) { },
findAll: function (store, type) { }
});
Related
I've come unstuck when trying to fetch a single record using Ember Data 2.
The server is designed to respond to a GET request like this:
GET http://server/api/results/1
with this as a result:
{
"results" : [
{
"id": 1,
"catname": "Category 1",
}
]
}
The Ember route code looks like this:
export default Ember.Route.extend({
model: function() {
return this.store.find('game',12);
}
});
The problem is that there doesn't appear to be a network request going out (a previous findAll fetch has worked, so I don't think it's the adapter), and there is an error I have not been able to find informaiton on:
Uncaught TypeError: Cannot set property'crossDomain' of undefined
Does anyone have any idea what this could be, of hint at how I might track this down?
In 1.13 new methods was introduced. You should use findRecord instead of find.
Also, ember expects following response when fetching a single object:
{
"result" :
{
"id": 1,
"catname": "Category 1",
}
}
I want to make an API call for searching that looks like this:
https://myapi.com/search/<query>/<token>
where query is the search term and token (optional) is an alphanumeric set of characters which identifies the position of my latest batch of results, which is used for infinite scrolling.
This call returns the following JSON response:
{
"meta": { ... },
"results" {
"token": "125fwegg3t32",
"content": [
{
"id": "125125122778",
"text": "Lorem ipsum...",
...
},
{
"id": "125125122778",
"text": "Dolor sit amet...",
...
},
...
]
}
}
content is an array of (embedded) items that I'm displaying as search results. My models look like this:
App.Content = Em.Model.extend({
id: Em.attr(),
text: Em.attr(),
...
});
App.Results = Em.Model.extend({
token: Em.attr(),
content: Em.hasMany('App.Content', {
key: 'content',
embedded: true
})
});
In order to make that API call, I figured I have to do something like this:
App.Results.reopenClass({
adapter: Em.RESTAdapter.create({
findQuery: function(klass, records, params) {
var self = this,
url = this.buildURL(klass) + '/' + params.query;
if (params.token) {
url += '/' + params.token;
}
return this.ajax(url).then(function(data) {
self.didFindQuery(klass, records, params, data);
return records;
});
}
}),
url: 'https://myapi.com/search',
});
then somewhere in my routes do this:
App.Results.fetch({query: 'query', token: '12kgkj398512j'}).then(function(data) {
// do something
return data;
})
but because the API returns a single object and Em.RESTAdapter.findQuery expects an array, an error occurs when Ember Model tries to materialize the data. So how do I do this properly? I'm using the latest build of Ember Model.
By the way, I'm aware that it would be much more convenient if the API was designed in a way so I can just call App.Content.fetch(<object>), which would return a similar JSON response, but I would then be able to set the collectionKey option to content and my data would be properly materialized.
You simply need to override your models load() method to adjust the payload hash to what Ember.Model wants. There are no serializers in Ember.Model. There is both a class level load for handling collections and an instance level load for loading the JSON specific to a single model. You want to override the instance level load method to wrap the content key value in an array if its not one already.
I have been using Ember.Mode quite heavily and enhanced it for a number of my use cases and submitted PR's for both fixes and enhancements. Those PRs have been sitting there for a while with no response from the maintainers. I have now moved to Ember.Data which has been 'rebooted' so to speak and having a lot better result with it now.
I would strongly suggest walking away from Ember.Model as it appears dead with the new pragmatic direction Ember Data has taken and because the project maintainer doesn't appear to have any interest in it anymore.
Consider this Ember JS Model:
App.User = DS.Model.extend({
firstName: DS.attr('string')
});
I am able to successfully save the model on the server using this as an XHR request:
{
"user": {
"first_name":"dude"
}
}
but for some reason it gives me an error while returning this XHR response:
{
"id":1,
"user":{
"first_name":"dude"
},
"createdAt":"2013-04-12T03:13:52.382Z",
"updatedAt":"2013-04-12T03:13:52.382Z"
}
The error says: Your server returned a hash with the key id but you have no mapping for it
Ember expects the output to look like:
{
"user": {
"id":1,
"first_name":"dude",
"createdAt":"2013-04-12T03:13:52.382Z",
"updatedAt":"2013-04-12T03:13:52.382Z"
}
}
I think the problem lies in the request itself, but I'm not sure.
Note that I'm using the Sails API as my backend.
You can use a controller to marshal the data format to whatever you need-- but this raises an interesting question about adding support for different front-end conventions to the API blueprints. Right now, Sails.js API blueprints support Backbone out of the box, but obviously that doesn't do you a lot of good if you're using Ember :) I created an issue for that here https://github.com/balderdashy/sails/issues/317.
Here's a hacky example of how you'd use a custom controller to send back data in this format using Sails today:
// api/controllers/UserController.js
module.exports = {
// Create action: (e.g. using default route, you'd POST to /user/create)
create: function (req,res) {
// Grab attributes from request using Ember conventions
var newAttributes = req.param('user');
// Create the user object in the datastore
User.create(newAttributes, function (err, newUser) {
// If there was an error, handle it
if (err) return res.send(err,500);
// Respond with the user object using Ember conventions
res.json({
user: newUser
});
});
}
};
That's a weirdly formatted JSON response. Do you have access to the server?
Ember expects the response as a a hash with root keys
{
"user": {
"id":1,
"first_name":"dude",
"createdAt":"2013-04-12T03:13:52.382Z",
"updatedAt":"2013-04-12T03:13:52.382Z"
}
}
When using Ember.StateManager, the most common transition between Em.States involve some parameter or another. Currently, I am using instance variables within the StateManager to pass parameters between States, when I do go from one state to another using goToState. This seems incredibly ugly to me. Is there a reason there is not a more standard way of passing parameters? Or should I use a different pattern.
For example,
App.stateManager = Em.StateManager.create({
initialState: 'listContacts',
listContacts: Em.ViewState.create({
...
actionSelectContact: function(manager, context) {
manager.set('selectedContact', context);
manager.goToState('showContact');
}
}),
showContact: Em.ViewState.create({
enter: function(manager, transition) {
var contactToShow = manager.get('selectedContact');
...
}
...
})
})
Is there a better way to do this parameter passing between states?
Tom Dale just added a transitionTo method to deal with this. transitionTo takes a context object along with the name of the target state. Now within your action you could do something like,
viewStates = Ember.StateManager.create({
showingPeople: Ember.ViewState.create({
view: ContactListView
}),
showDetailAction: function(mgr, selectedPerson) {
mgr.transitionTo('showingPersonDetail', selectedPerson);
},
showingPersonDetail: Ember.ViewState.create({
setupContext: function(manager, context) {
this.set('person', context);
},
view: PersonDetailView
})
})
You could also get more fancier and pass parameters for multiple states along the way like,
stateManager.transitionTo(['planters', { company: true }], ['nuts', { product: true }]);
I'm not an Ember expert, but I think you could achieve this using stateManager.send() method where the second argument will be the object you want to pass between states.
Most of your answer is in the Ember.StateManager documentation.
There was a pull request in Ember talking about extra params in goToState() method here, but it has been closed because goToState() should only be used internally as joewest says here with tomdale:
goToState should only be called from within a state. To accomplish this, just implement an action that takes additional parameters, and have it call goToState for you.
I have a model built from a JSON object.
// extend the json model to get all props
App.Model = Ember.Object.extend(window.jsonModel);
I want to automatically save the model when anything is updated. Is there any way I can add an observer to the whole model?
EDIT: // adding the solution I currently go
For now I do:
// XXX Can't be right
for (var prop in window.jsonModel) {
if (window.jsonModel.hasOwnProperty(prop)) {
App.model.addObserver(prop, scheduleSave);
}
}
This is a large form, which means I'm adding tons of observers – it seems so inefficient.
A firebug breakpoint at Ember.sendEvent() reveals that there are events called App.model.lastName:change being sent. I could hack in an intercept there, but was hoping for an official way.
You can bind to isDirty property of subclass of DS.Model. The isDirty changes from false to true when one of model properties changes. It will not serve well for all cases because it changes only once until reset or committed, but for your case -
I want to automatically save the model when anything is updated. Is there any way I can add an observer to the whole model?
it may work fine.
From the article:
autosave: function(){
this.save();
}.observes('attributes'),
save: function(){
var self = this,
url = this.get('isNew') ? '/todos.json' : '/todos/'+this.get('id')+'.json',
method = this.get('isNew') ? 'POST' : 'PUT';
$.ajax(url, {
type: 'POST',
// _method is used by Rails to spoof HTTP methods not supported by all browsers
data: { todo: this.get('attributes'), _method: method },
// Sometimes Rails returns an empty string that blows up as JSON
dataType: 'text',
success: function(data, response) {
data = $.trim(data);
if (data) { data = JSON.parse(data); }
if (self.get('isNew')) { self.set('id', data['todo']['id']); }
}
});
},
isNew: function(){
return !this.get('id');
}.property('id').cacheable(),
I had the same requirement, and not finding a suitable answer, I implemented one.
Try this: https://gist.github.com/4279559
Essentially, the object you want to observe all the properties of MUST be a mixed of Ember.Stalkable. You can observe the properties of that object as 'item.#properties' (or, if you bake observers directly on the Stalkable, '#properties' alone works. "#ownProperties", "#initProperties" and "#prototypeProperties" also work, and refer to (properties that are unique to an instance and not defined on any prototype), (properties that are defined as part of the create() invocation), and (properties that are defined as part of the class definition).
In your observers, if you want to know what properties changed and invoked the handler, the property "modifiedProperties", an array, will be available with the names of the changed properties.
I created a virtual property _anyProperty that can be used as a dependent key:
import Ember from 'ember';
Ember.Object.reopen({
// Virtual property for dependencies on any property changing
_anyPropertyName: '_anyProperty',
_anyProperty: null,
propertyWillChange(keyName) {
if (keyName !== this._anyPropertyName) {
this._super(this._anyPropertyName);
}
return this._super(keyName);
},
propertyDidChange(keyName) {
if (keyName !== this._anyPropertyName) {
this._super(this._anyPropertyName);
}
return this._super(keyName);
}
});