Ember data is still not at version 1.0 and thus I decided to use Ember without Data models.
I have my own models, and those are created by the route model function.
However maintaining state between the frontend objects and the backend objects is a nightmare.
Especially when one route uses another routes models.
How can this be achieved, should I write my own store and model find method?
Should I use Ember Data (even though it's not at version 1.0 ?) perhaps an ETA on Ember Data 1.0 ?
write code to update the models on the frontend each time I change a model?
Another method?
Is what I'm doing best practices or should I be doing it differently?
My gut feeling is that without using Ember Data I should write my own store. I'd love to get feedback from some of you guys.
Example of a model:
App.Person = Ember.Object.extend(App.Serializable,Em.Copyable,{
user_email : null //used in routing dynamic segements and as old email when making changes to email
,first_name: null
, last_name: null
, fullname : function () {
return this.first_name + ' ' + this.last_name;
}.property('first_name','last_name').cacheable()
};
App.Person.reopenClass({
createRecord: function(data) {
return App.Person.create({
user_email : data.email
, first_name: data.first_name
, last_name : data.last_name
}});
Example of how I load the class models:
App.UsersRoute = App.AuthenticatedRoute.extend( {
model : function () {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.getJSON('/users').then(function(usersData) {
var userObjects = [];
usersData.forEach(function (data) {
userObjects.pushObject(App.Person.createRecord(data));
});
resolve( userObjects);
},function(error) {
reject(error);
});
})
},
Subroutes use the model:
App.UsersAvailableRoute = App.AuthenticatedRoute.extend( {
model : function () {
return {
allUsers : Ember.ArrayController.create({
content : this.modelFor('users').filter( function (user) {
return user.availablity === 100
}),
Example of how I update the model in a controller:
App.UsersAvailableController = Ember.ArrayController.extend({
needs : ['users']
,applyPersonAssign : function (person,need,project) {
need.set('allocated',person);
var updateObject = Ember.copy(project,true);
if (Ember.isEmpty(need.get('inProject'))) {
updateObject.projectNeeds.pushObject(need);
}
return $.ajax({
url: '/projects/' + updateObject.get('codename'),
"type": "PUT",
"dataType": "json",
data: updateObject.serialize()
})
You don't necessarily need to recreate the Ember Data store. Ember works just fine with POJOs, you can also wrap your POJOs in an Ember Object to give you some fun built in features.
That being said creating a custom adapter which caches results could be convenient.
Here's an example where I create an adapter that supports caching. You can slowly build on the concept for all of the basic things you need.
App.FooAdapter = Ember.Object.extend({
cache:{},
find: function(id){
var self = this,
record;
if(record = this.cache[id]){
return Ember.RSVP.cast(record);
}
else
{
return new Ember.RSVP.Promise(function(resolve){
resolve($.getJSON('http://www.foolandia.com/foooo/' + id));
}).then(function(result){
record = self.cache[id] = App.Foo.create(result);
return record;
});
}
}
});
In the examples below, I use the container to register the adapter on all of my routes/controllers so I had lazy easy access to it.
http://emberjs.jsbin.com/OxIDiVU/742/edit
If you always want it to be a promise:
http://emberjs.jsbin.com/OxIDiVU/740/edit
Reusability
The example above may make it look like you'd have to do a ton of work, but don't forget, Ember is super reusable, take advantage of the magic.
App.MyRestAdapter = Em.Object.extend({
type: undefined,
find: function(id){
$.getJSON('http://www.foolandia.com/' + this.get('type') + '/' + id
}
});
App.FooAdapter = App.MyRestAdapter.extend({
type: 'foo' // this would create calls to: http://www.foolandia.com/foo/1
});
App.BarAdapter = App.MyRestAdapter.extend({
type: 'bar' // this would create calls to: http://www.foolandia.com/bar/1
});
This is the basic idea of what Ember Data/Ember Model is. They've tried to create a ton of defaults and built in coolness, but sometimes it's overkill, especially if you are just consuming data and not doing CRUD.
Example: http://emberjs.jsbin.com/OxIDiVU/744/edit
Also you can read this (says the same stuff):
How do you create a custom adapter for ember.js?
Where I work we are using Ember Data and Ember CLI despite them being rather unstable. So far Ember Data hasn't caused too much pain and suffering here. The store is pretty easy to understand, and the documentation on Ember for that facet of the framework is rather good. The one issue that I have been having has to do with dynamically sorting models, and as I modify the content of them, they reshuffle according to the changes I make, and somewhere along the road some really weird stuff happens, not sure if that's Ember Data's fault though.
In short, we've found some success using Ember Data, and can't complain about it if that's the route you wish to go.
If you're familiar with Ruby, Rails is a great solution for a backend.
The ember community has support for rails with the ember-rails gem which lets you use rails as a means to serve JSON.
Getting started
Add the gem to your application Gemfile:
gem 'ember-rails'
gem 'ember-source', '~> 1.9.0' # or the version you need
Run bundle install
Next, generate the application structure:
rails generate ember:bootstrap
Restart your server (if it's running)
Building a new project from scratch
Rails supports the ability to build projects from a template source ruby file.
To build an Ember centric Rails project you can simply type the following into your command line:
rails new my_app -m http://emberjs.com/edge_template.rb
To install the latest builds of ember and ember-data. It should be noted that the
examples in the getting started guide
have been designed to use the released version of ember:
rails generate ember:install
Then all you need to do is render json in your controllers, like this
class ProjectsController < ApplicationController
def index
respond_to do |format|
format.json { render json: Project.all }
end
end
def show
respond_to do |format|
format.json { render json: Project.where(name: params[:name])}
end
end
end
make sure to update your serializers
class ProjectSerializer < ApplicationSerializer
attributes :id, :name, :description, :imgUrl, :deployUrl
end
setup your routes
EmberRailsBlog.ProjectsRoute = Ember.Route.extend({
model: function(){
return this.store.find('project');
}
});
and finally your model
var attr = DS.attr;
EmberRailsBlog.Project = DS.Model.extend({
name: attr(),
description: attr(),
imgUrl: attr(),
deployUrl: attr()
});
Related
I am trying to make a request from the store for a model based on the result of a previous model request. Please find the code below:
For the route:
model(params) {
var id = params.framework_id;
var main = this;
return Ember.RSVP.hash({
question: this.store.query('question', {orderBy: 'framework', equalTo: id}),
framework: this.store.find('frameworks', id),
frameworks: this.store.findAll('frameworks')
})
}
Then in the route there is a setupController:
setupController: function(controller, model) {
this._super(controller, model);
var main = this;
...
controller.set("frameworkName", model.framework.get('name'));
var includedFramework = model.framework.get('includedFramework');
var includedFrameworkModel = this.store.find('frameworks', includedFramework);
Ember.Logger.info(model.framework)
Ember.Logger.info(includedFrameworkModel);
if (model.framework.get('includedFramework') != undefined) {
var linked = main.store.find("frameworks", model.framework.get('includedFramework'));
controller.set("linkedFramework", {id: linked.get('id'), name: linked.get('name')});
}
}
In the controller setup, using model.framework.get('name') works without a problem. mode.framework.get('includedFramework') works fine and returns an ID to another framework that is stored in the framework model. I then intend to pull the "included framework" in the store with another store.find request. Unfortunately this second store.find doesn't return the model record in the same way as the first. Here is an inspector view of what each request returns:
model.framework -
includedFrameworkModel -
Any help is greatly appreciated!
Well, every call to ember-data that returns something that may require a request to the server, like find(), findAll(), query(), save(), and async relationships returns a PromiseObject or a PromiseArray.
It works the same way for objects and arrays, so just lets describe how arrays work.
A PromiseArray is both, a Promise and a ArrayProxy.
This is very useful because you can work with it in both ways, depending on your situation.
Because the request to the server may take some time, the resulting ArrayProxy part is often empty, and will be populated with data later. This is very useful because your handlebars template and computed properties will update when the ArrayProxy changes.
The Promise part of the PromiseArray will resolve as soon the data are received from the server, with an actual array, that also may update later when you do further changes on your data.
The ember router however will wait for the promise to be resolved before it loads the route, which allows you to specify a loading substrate.
This is why model.framework is different in setupController. It's not the result of the .find() but the result of the resolved promise. Thats basically what Ember.RSVP.hash does for you.
Generally I recommend you two things.
Don't store a model id on a model, but use a relationship.
Don't call the store in .setupController(). Do all your requests in the .model() hook and use the promise chain to do so.
Maybe take this as a inspiration:
return Ember.RSVP.hash({
question: this.store.query('question', {orderBy: 'framework', equalTo: id}),
framework: this.store.find('frameworks', id),
frameworks: this.store.findAll('frameworks')
}).then(({question, framework, frameworks}) => {
let includedFramework = framework.get('includedFramework');
let includedFrameworkModel = this.store.find('frameworks', includedFramework);
return {question, framework, frameworks, includedFrameworkModel};
});
var includedFrameworkModel = this.store.find('frameworks', includedFramework);
The store.find() method will return the promise, the object was not resolved when you print in log.
Changed your script to something like this.
this.store.find('frameworks', includedFramework).then(function(includedFrameworkModel) {
Ember.Logger.info(includedFrameworkModel);
});
Here what my json data look like:
[{"id":1,"iam":1,"youare":2,"lat":50.8275853,"lng":4.3809764,"msgbody":"Lorem ipsum lorem ipsum lorem ipsum"}]
Here's my Ember code:
window.Messages = Ember.Application.create();
Messages.Store = DS.Store.extend({
revision: 11
});
Messages.MessagesRoute = Ember.Route.extend({
setupControllers: function(controller) {
controller.set('content', Messages.Message.find());
}
});
Messages.Message = DS.Model.extend({
msgbody: DS.attr('string')
});
Messages.MessagesController = Ember.ArrayController.extend({
content: []
});
The thing is my json data live in /app_dev.php/messages not in /messages/ ...
I am just trying to do just a successful get request but I can't manage... Could you tell what I am doing wrong so I can get some grasp of the Ember syntax?
EDIT Thanks for your answers.Just to inform that after lots of effort to make something trivial, I tried Angular and it seems to do the job better,faster and easier. So I'm switching frameworks.
You'll want to use the namespace option on the adapter. This can be specified as followed and then when you create the store use the MyApp adapter.
MyApp.Adapter = DS.RESTAdapter.extend({
namespace: 'app_dev.php'
});
I think that it is better to specify file names via the url option:
App.Store = DS.Store.extend({
revision: 11,
adapter: DS.RESTAdapter.create({
url: "/app_dev.php"
})
});
The structure of your JSON should be as follows:
{
messages: [
{"id":1,"iam":1,"youare":2,"lat":50.8275853,"lng":4.3809764,"msgbody":"ipsum "},
{"id":2,"iam":1,"youare":2,"lat":50.8275853,"lng":4.3809764,"msgbody":"Lorem "}
]
}
See ember guide for further reference.
Ember requires that you use what they call "type keys" in order to successfully read JSON, in other words you need to put the name of a model prior to the object itself for Ember to recognize that is of that type.
In my case, I had made a Java/Spring Backend and did not want to add this to my objects, to get around this in ember you can create a Serializer file, documentation for Ember Serializers, specifically the RESTSerializer can be found here.
To add the typeKey to incoming JSON overide the normalizePayload hook. Here is an example of how I did that:
normalizePayload: function(type, payload) {
var json = {};
json[type.typeKey] = payload;
return json;
}
If you hadn't put your typeKey's on the JSON on the way out, you'll probably experience similar problems when you PUT or POST data, so you will probably need to overload the serializeIntoHash hook to remove the typeKey's on outgoing data. Here is an example of what worked for me on that:
serializeIntoHash: function(hash, type, record, options){
Ember.merge(hash, this.serialize(record, options));
}
Hope that helps! I see you are switching to Angular (Great decision, I prefer Angular 100x over Ember but my work requires I use Ember currently, so I'm fighting through it.) But hopefully this can help someone else who is having similar issues and either wants or is forced to use Ember.
Is there a way to built more complex Model in Backbone.js, let me explain by an example :
This is a Json Session object :
{
id: "17",
notes: "",
start: "2012-10-18T15:57:41.511Z",
end: "2012-10-18T19:22:31.875Z",
sessionType: {
id: "1",
name: "Life Style",
}
}
When retrieving a Session Object from the server, I would like to have a SessionType Backbone.Model in order to add some business logic around this object.
So far I'm only able to retrieve an Object Session with a dummy SessionType, I can't add any logic on it because It doesn't belong to any Backbone model.
You can try this:
window.SessionType = Backbone.Model.extend({
initialize:function () {
},
});
Then in your session model, have a method:
window.Session = Backbone.Model.extend({
initialize:function () {
},
getSessionType () {
return new SessionType(this.get('sessionType'));
}
});
Now you can call the getSessionType() method which returns a model that can have your logic.
#Amulya is 100% correct. However, if you want the Session Model without having to call getSessionType(), I would look at using the the built in parse method and creating your model from there.
If your Session model is related to your model, I would look at using Backbone Relational. Since Backbone does not handle relationships, the plugin listed above does a fine job in filling the gap without too much manual labour.
I have an application that saves a user's search criteria in localStorage, where each saved search is represented as an instance of an Ember.js model:
Checklist.SavedSearch = DS.Model.extend({
id: DS.attr('string'),
filters: DS.attr('string')
});
When the "save" button is pressed, the controller creates a model instanced and creates a record for it:
Checklist.savedSearchController = Ember.ArrayController.create({
[..]
save: function(view) {
var saved_seach = Checklist.SavedSearch.createRecord({
id: 'abcd',
filters: '<json>'
});
Checklist.local_store.commit();
}
});
Checklist.local_store is an adapter I created (this is unsurprisingly where the problem probably begins) that has a basic interface that maps createRecord, updateRecord, etc. to a bunch of get/set methods that work with localStorage (loosely based on a github fork of ember-data). The adapter appears to work fine for some basic tests, particularly as findAll has no issues and returns values added manually to localStorage.
Here is the relevant method within Checklist.local_store:
createRecord: function(store, type, model) {
model.set('id', this.storage.generateId);
var item = model.toJSON({associations: true});
this.storage.setById(this.storage_method, type, id, item);
store.didCreateRecord(model, item);
}
The problem is that when createRecord is called by the controller, absolutely nothing occurs. Running it through the debugger, and logging to console, seems to show that the method isn't called at all. I imagine this is a misunderstanding on my part as to how Ember.js is supposed to work. I'd appreciate help on why this is happening.
I come from a ruby and php background, and have perhaps foolishly dived straight in to a JS framework, so any other comments on code style, structure and anything in general are welcome.
Ember Data doesn't change createRecord on the controller so it shouldn't behave any differently. It's possible that there was something related to this in the past, but it's certainly not the case anymore.
I'm writing a practice Backbone app, with Rails backend API, and I'm confused about the behavior of save on Backbone models.
Let's say a Team has many Players, and I want to save a team with numerous players in a single POST.
So in Rails I have:
class Team < ActiveRecord::Base
has_many :players
accepts_nested_attributes_for :players
end
class Player < ActiveRecod::Base
belongs_to :team
end
and for backbone client, I have a Player model and a Players collection defined (not shown)
and then the containing Team model (NOTE: no Teams collection)
Demo.Models.Team = Backbone.Model.extend({
urlRoot: '/teams',
defaults: {
'team_size': 12
},
initialize: function() {
this.players = new Demo.Collections.Players());
},
toJSON: function() {
var json = _.clone(this.attributes);
json.players_attributes = this.players.map(function(player) {
return player.toJSON();
});
return json;
}
}
When I examine my stringified JSON in the browser, everything looks good:
{"team_size":12, "players_attributes":[{"name":"Fred"},{"name":"Jim" },{"name":"Mark"}]}
Checking the server logs, the lone top level attribute ('team size') is repeated, once at the top level, and then repeated under a root key.
Started POST "/teams" for 127.0.0.1 at 2012-06-07 13:39:40 -0400
Processing by TeamsController#create as JSON
Parameters: {
"team_size"=>12, "players_attributes":[{"name":"Fred"},{"name":"Jim" },{"name":"Mark"}]},
"team"=>{"team_size"=>12}
}
I have a few questions:
What's the best way to ensure the player_attributes are nested inside the root key? I (So that I can do a nested save inside TeamController, in the standard rails manner: (i.e. Team.create(params[:team]) ) I can accomplish this with some javascript hackery inside toJSON, but I'm guessing there's an easier, cleaner way.
Is this standard, desirable behaviour? To send duplicates of attributes like this? I guess there's no harm, but it doesn't smell right.
Am I not defining the url / urlRoot correctly or some such?
thanks
1- You have to override the toJSON method in order to include the model name as the root of the JSON element sent to the server.
toJSON: function() {
return { team: _.clone( this.attributes ) }
},
Since you are already messing and overriding this method I don't see any reasons not to go this way.
2- This is a very strange behavior you're describing. Try:
class Team < ActiveRecord::Base
self.include_root_in_json = false
end
It will probably eliminate Rails duplicate params parsing. Another advantage you get from this is that Rails won't include the team as a root element of its generated JSON to the client.
3- Your definition of urlRoot is just fine.
I arrived here while looking for same issue. So even it's an old question I think it's worth giving the answer.
I actually found a Rails setting that explain these duplicate attributes: wrap_parameters
http://apidock.com/rails/v3.2.13/ActionController/ParamsWrapper/ClassMethods/wrap_parameters
Just set it to an empty array, and rails won't try to wrap parameters coming from your JSON requests.
Although you can use the toJSON hack mentioned by others, this is actually not such a good idea. For one, it produces an inconsistent result between sync and save with {patch: true} (this inconsistency is because the sync method calls toJSON if you don't patch, but doesn't call toJSON if you have patch set to true)
Instead, a better solution is to use a patched version of Backbone that overload the sync method itself. The backbone-rails gem does this automatically, or you can pull backbone_rails_sync.js into your own app. A more complete answer to this question can be found here: Backbone.js and Rails - How to handle params from Backbone models?