Why is Backbone model sending duplicate attributes to server on save? - javascript

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?

Related

How to send Rails parent/child to front end JS so it's accessible as a nested object

In backend, my object relationship is that an Item has_many Options. I'd like to be able to access all the attributes on the item and its child options as a hash in the front end:
items = [
{
id: 1,
item_attribute_name: item_attribute_value,
options: [
{id: 1, option_attribute_name: option_attribute_value},
{id: 2, option_attribute_name: option_attribute_value},
]
},
{
id: 2,
item_attribute_name: item_attribute_value,
options: []
}
]
I'm sending data either via a json object in response to an ajax request or using the handy gon gem. I noticed that if I were JUST interested in sending the parent items, the formatting automatically happens such that I can just send back Item.all and in the front end, get an array of items with each item being a hash that represents its attributes exactly as I want.
But if I want to send the children is there a standard way of doing it? I realize I can construct the child attributes myself as below, but wondering if there's a more straight forward direct way.
How I would make this work by constructing the child attributes:
items = Item.all
items.each do |i|
child_attr = {"options" => i.options }
i.attributes.merge(child_attr)
end
A totally acceptable answer, by the way, is that there's no... automagical way of doing this without doing what I'm doing now, which is converting each parent object to attributes in backend, and then stitching together the child attributes.
I'm only asking this question, frankly, because it'd be nice to keep the object relationships in the backend for reuse elsewhere, if possible, rather turning things into a hash.
I think the only way to get the expected result is with some monkey patching. So you can use a serializer, or include, or use ActiveModel::Serializers::JSON which is included by default in your models.
For exemple with ActiveModel::Serializers::JSON, you can do something like:
items = Item.all
items.map! do |i|
i.serializable_hash(include: { options: {} })
end
This is due to rails eager loading, which avoids having to load all children from an association(has_many for example). Where you'd like to serialize is up to your use case.

Backbone ignore posting specific attributes on save? [duplicate]

I'm using Backbone with Rails. I have a model that I can create and destroy just fine. When I edit, though, I get this error:
Can't mass-assign protected attributes: created_at, id, updated_at
That makes sense. Those attributes are protected and they should be protected. Backbone shouldn't be trying to update these attributes, but Backbone doesn't know better.
One option, of course, would be to remove params[:created_at], etc. in my Rails controller, but I can imagine that getting really un-DRY pretty quick, and plus it just seems wrong to have to do that.
Is there a way for me to tell Backbone not to include these attributes in its forms?
Either don't send them to the client so that your Backbone model never knows about them or override toJSON in your model to exclude them.
The default toJSON implementation is very simple:
toJSON: function() {
return _.clone(this.attributes);
}
so you can replace it with this:
toJSON: function() {
var attrs = _(this.attributes).clone();
delete attrs.created_at;
delete attrs.updated_at;
return attrs;
}
You could even monkey patch that right into Backbone.Model.prototype if that made sense to you.
The downside of altering toJSON is that toJSON tends to do double duty in Backbone:
toJSON is used to serialize models and collections for the server.
toJSON is used to serialize models and collections for views.
If you still want to use updated_at and created_at in views then I'd recommend adding another method, say serialize_for_view, that does what the standard toJSON does:
serialize_for_view: function() {
return _(this.attributes).clone();
}
and then use things like var html = this.template({m: this.model.serialize_for_view()}) to build your view's HTML. You could also monkey patch serialize_for_view into Backbone.Model.prototype if you wanted to use it everywhere.
I found that putting
model.unset("created_at");
model.unset("updated_at");
model.save()
fixed the problem. This won't work if you need those attributes, but if they are not needed, this works.

Waiting for meteor collection to finish before next step

I have a Meteor template that should be displaying some data.
Template.svg_template.rendered = function () {
dataset_collection = Pushups.find({},{fields: { date:1, data:1 }}, {sort: {date: -1}}).fetch();
a = moment(dataset_collection[0].date, "YYYY/M/D");
//more code follows that is also dependent on the collection being completely loaded
};
Sometimes it works, sometimes I get this error:
Exception from Deps afterFlush function: TypeError: Cannot read property 'date' of undefined
I'm not using Deps in any context. As I understand it, the collection is being referenced before it is completely finished loading.
I therefore would like to figure out how to simply say "wait until the collection is found before moving on." Should be straightforward, but can't find an updated solution.
You are right, you should ensure that code depending on fetching the content of a client-side subscribed collection is executed AFTER the data is properly loaded.
You can achieve this using a new pattern introduced in Meteor 1.0.4 : https://docs.meteor.com/#/full/Blaze-TemplateInstance-subscribe
client/views/svg/svg.js
Template.outer.onCreated(function(){
// subscribe to the publication responsible for sending the Pushups
// documents down to the client
this.subscribe("pushupsPub");
});
client/views/svg/svg.html
<template name="outer">
{{#if Template.subscriptionsReady}}
{{> svgTemplate}}
{{else}}
Loading...
{{/if}}
</template>
In the Spacebars template declaration, we use an encapsulating outer template to handle the template level subscription pattern.
We subscribe to the publication in the onCreated lifecycle event, and we use the special reactive helper Template.subscriptionsReady to only render the svgTemplate once the subscription is ready (data is available in the browser).
At this point, we can safely query the Pushups collection in the svgTemplate onRendered lifecycle event because we made sure data made its way to the client :
Template.svgTemplate.onRendered(function(){
console.log(Pushups.find().fetch());
});
Alternatively, you could use the iron:router (https://github.com/iron-meteor/iron-router), which provides another design pattern to achieve this common Meteor related issue, moving subscription handling at the route level instead of template level.
Add the package to your project :
meteor add iron:router
lib/router.js
Router.route("/svg", {
name: "svg",
template: "svgTemplate",
waitOn: function(){
// waitOn makes sure that this publication is ready before rendering your template
return Meteor.subscribe("publication");
},
data: function(){
// this will be used as the current data context in your template
return Pushups.find(/*...*/);
}
});
Using this simple piece of code you'll get what you want plus a lot of added functionalities.
You can have a look at the Iron Router guide which explains in great details these features.
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md
EDIT 18/3/2015 : reworked the answer because it contained outdated material and still received upvotes nonetheless.
This is one of those problems that I really wish the basic meteor documentation addressed directly. It's confusing because:
You did the correct thing according to the API.
You get errors for Deps which doesn't point you to the root issue.
So as you have already figured out, your data isn't ready when the template gets rendered. What's the easiest solution? Assume that the data may not be ready. The examples do a lot of this. From leaderboard.js:
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
Only if player is actually found, will player.name be accessed. In coffeescript you can use soaks to accomplish the same thing.
saimeunt's suggestion of iron-router's waitOn is good for this particular use case, but be aware you are very likely to run into situations in your app where the data just doesn't exist in the database, or the property you want doesn't exist on the fetched object.
The unfortunate reality is that a bit of defensive programming is necessary in many of these cases.
Using iron-router to wait on the subscription works, but I like to keep subscriptions centrally managed in something like a collections.js file. Instead, I take advantage of Meteor's file load order to have subscriptions loaded before everything else.
Here's what my collections.js file might look like:
// ****************************** Collections **********************************
Groups = new Mongo.Collection("groups");
// ****************************** Methods **************************************
myGroups = function (userId) {
return Groups.find({"members":{$elemMatch:{"user_id":userId}}});
};
// ****************************** Subscriptions ********************************
if(Meteor.isClient){
Meteor.subscribe("groups");
}
// ****************************** Publications *********************************
if(Meteor.isServer){
Meteor.publish("groups", function () {
return myGroups(this.userId);
});
}
I then put collections.js into a lib/ folder so that it will get loaded prior to my typical client code. That way the subscription is centralized to a single collections.js file, and not as part of my routes. This example also centralizes my queries, so client code can use the same method to pull data:
var groups = myGroups(Meteor.userId());

Angular / Breeze - camelCasing in model binding

I am just starting to learn Angular, and have been working with John Papa's course on Pluralsight to try and get my head around it. I was having a problem binding some data to a page, and it turned out to be an incorrect casing.
I had:
<small>{{s.timeslot.name}}</small> at <small>{{s.room.name}}</small>
when I should have had:
<small>{{s.timeSlot.name}}</small> at <small>{{s.room.name}}</small>
That's fine, but I just can't understand why.
The data is returned from Web API with this method (note no camelcase on timeslots):
[HttpGet]
public object Lookups()
{
var rooms = _repository.Rooms;
var tracks = _repository.Tracks;
var timeslots = _repository.TimeSlots;
return new { rooms, tracks, timeslots };
}
An angular service called datacontext.js has the following object to map between (at least to my understanding) Web API names and Breeze entity names.
var entityNames = {
attendee: 'Person',
person: 'Person',
speaker: 'Person',
session: 'Session',
room: 'Room',
track: 'Track',
timeslot: 'TimeSlot'
}
There are some further functions that involve caching data, and extending the model on the client, but nowhere is there a reference to timeSlot (with camelCase).
What is happening here, is a convention for HTML casing being enforced by Angular?
EDIT: Thanks to Ward for the answer. For anyone following the same course who has a similar question, breeze.NamingConvention.camelCase.setAsDefault() is called in entityManagerFactory.js.
The problem is a bit of "sloppiness" in the spelling of "TimeSlot" in various places.
Your binding to "s" is a binding to the Session type. The Session type itself dictates the spelling of any of its navigation properties. Everything else is irrelevant.
The metadata for Session is generated on the server based on the C# "Session" class. There you will find that the navigation property is spelled "TimeSlot".
Your client is using the Breeze NamingConvention.camelCase to translate between the preferred PascalCasing of C# and the preferend camelCasing in JavaScript apps. Therefore, on the Breeze client you should expect "TimeSlot" navigation property to become "timeSlot".
Entity names are not translated by the NamingConvention (at this time). The C# type, "TimeSlot" is also spelled "TimeSlot" for the JavaScript type.
Every other spelling everywhere else is irrelevant for this binding.
You may find the "Query result debugging" documentation topic helpful.

Backbone-relational.js + Backbone.View(s)

The question: The documentation is scarce, and I'm something of a noob -- can anyone confirm the proper (assuming there is one) way to bind Backbone.Views to instances of Backbone.RelationalModel (from backbone-relational.js) for updating/rendering to the dom? I've tried a handful of different approaches, based on the normal Model/View binding in Backbone, with little success.
The backstory (/more info):
I'm learning the ropes with Backbone.js, and have had to pick up a lot over the past week. If I'm missing something obvious (which is highly likely -- including the "right" way to handle my problem below), please call me out.
I'm dealing with a mongodb-backed REST interface (that I don't have full control over -- or I would be re-architecting behavior on the server-side) that takes heavy advantage of nested dictionaries, so I've been reading up on how to best represent that in Backbone (while not breaking the great save() + server sync stuff that Backbone provides).
I've seen two options: backbone-relational and ligament.js.
I've started with backbone-relational.js, and have RelationalModels (backbone-relational's replacement for Backbone's standard Model) created for the various dictionaries in the tree that gets handed back by REST interface. The relationships between them are defined, and console logging the JSON from each model (in their respective initialize functions) shows that they're all being called/loaded up correctly off the server on a fetch() command at the overall collection level.
So, that's all great.
Problem: I've got views "listening" for updates on each of those models (and bound functions that should render templates on the dom), and they never "fire" at all (let alone render...). The main view fires on fetch(), no problem, loading the "top level" model and rendering it on the dom -- but the views that represent the "foreign key" models within that "top level" model never do (even though the data is DEFINITELY getting loaded into each model, as evidenced by the console logging on each model mentioned above).
Any insights would be greatly, greatly appreciated.
In direct response to Raynos reply below (thanks Raynos!):
If I defined a base url for the UpperLevelCollection with the UpperLevelModels existing at (UpperLevelCollection url)/(UpperLevelModel id) on the server, how would I map those LowerLevelCollections to dictionary keys within the one JSON dump for each UpperLevelModel from the server-side? In other words, could using collections within models properly handle a data dump from the server like this (obviously very simplified, but gets at the issue) AND properly save/update/sync it back?
[{
"some_key": "Some string",
"labels": ["A","List","Of","Strings"],
"content": [{
"id": "12345"
"another_key": "Some string",
"list": ["A","list","of","strings"],
},{
"id": "67890"
"another_key": "Some string",
"list": ["A","list","of","strings"],
}],
}]
Generally for nested dictionaries I take the following approach
var UpperLevelCollection = Backbone.Collection.extend({
model: UpperLevelModel
}),
UpperLevelModel = Backbone.model.extend({
initialize: function() {
this.nested = new LowerLevelCollection;
}
}),
LowerLevelCollection = Backbone.Collection.extend({
model: LowerLevelModel
}),
LowerLevelModel = Backbone.Model.extend({});
Just nest those collections inside models all the way down.
The problem might be that as you load new data into you ParentModel, your child collection AFAIK is not actually fetched, it's wiped and replaced by a new collection (see Backbone.HasMany.OnChange on line 584 in backbone-relational.js). Thus your own bindings on the child collection are gone.
This is, in my opinion, a weakness with backbone-relational. This behavior should be configurable, with an option where a slower find-and-update-approach is used instead of wipe-and-replace.

Categories

Resources