Re-load changed associated models on save using accepts_nested_attributes_for

I’m doing a save using accepts_nested_attributes_for and have this setup:

App.Foo = DS.Model.extend
    optionValues: DS.hasMany('optionValues')

App.OptionValue = DS.Model.extend
    value: DS.attr('string')

App.FooSerializer = DS.ActiveModelSerializer.extend DS.EmbeddedRecordsMixin,
    attrs:
	optionValues:	{
		serialize: 'records',
		deserialize: 'records'
	}

I’ve tried setting up my serializer in rails a few ways, either embedding the objects or including them and the id’s but no matter what the value objects aren’t updated properly. In Ember they remain in a dirty state.

If I add some debugging to the Foo & OptionValue serializers in Ember this is what I see when the rails serializer is set to embed :objects:

fooSerializer: extract [Class, function, Object, "1173", "updateRecord"]
fooSerializer: extractMeta [Class, function, Object]
fooSerializer: extractUpdateRecord [Class, function, Object, "1173", "updateRecord"]
fooSerializer: extractSave [Class, function, Object, "1173", "updateRecord"]
fooSerializer: extractSingle [Class, function, Object, "1173", "updateRecord"]
fooSerializer: normalizePayload [Object]
fooSerializer: normalize [function, Object, "foo"]
fooSerializer: normalizeLinks [Object]
fooSerializer: keyForRelationship ["values", "hasMany"]
fooSerializer: keyForAttribute ["values"]
optionValueSerializer: normalize [function, Object, "option_values"]
optionValueSerializer: normalizeLinks [Object]
optionValueSerializer: keyForAttribute ["value"]

But I never see any extracts being called for the values, I can test this by changing a value for an option value in the rails serializer and then inspecting in ember inspector and the value doesn’t get updated.

I’ve tried not embedding objects & setting it in the serializer in embed as appropriate (optionValues: { serialize: 'records', deserialize: 'ids' }) but get the same behaviour.

The strange thing is if I create a new Foo model and save that with new OptionValues then they get populated properly - the debug trace is the same - no calls to extract etc. on OptionValueSerializer. Though in the ember inspector I have the old null id OptionValues still sticking around.

Notes on versions:

DEBUG: Ember      : 1.7.0 ember.js?body=1:14464
DEBUG: Ember Data : 1.0.0-beta.10
1 Like

Also tried upgrading to Ember Data beta.11, and this is the response (when embed :objects in rails serializer) for reference:

{
    "foo": {
        "id":1174,
        "option_values":[
            {"id":19861,"value":"ffffff","foo_id":1174}
        ]
    }
}

Added an example here JS Bin - Collaborative JavaScript Debugging with both ways of returning the response you can see that the value doesn’t update for first option value to match the response and the isDirty flag isn’t cleared.

Note however that if you don’t edit one of the values first the random value in the response is updated, but as soon as the option value has the isDirty flag it isn’t.

I’ve figured out a hacky workaround, but don’t like it (for hopefully obvious reasons):

save: function() {
  model = this.get('model');
  model.get('optionValues').forEach(function(v) {
    if(v.get('isDirty')) {
      v.send('willCommit');
      v.set('_attributes', {});
    }
  });
  
  model.save().then(function() {
     model.get('optionValues').forEach(function(v) {
      v.send('didCommit');
    });
  });
}

See JS Bin - Collaborative JavaScript Debugging

1 Like

I’ve added a more comprehensive example and opened an issue on ember-data Saving with EmbeddedRecordsMixin does not update related models dirty attributes · Issue #2487 · emberjs/data · GitHub.

Edit: I obviously didn’t understand what you were asking at first :/. I’m not sure, I don’t usually embed the objects, instead only the ID’s.

Original Post: Here is how I do it in a specific object serializer:

import Ember from 'ember';
import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({
  serializeHasMany: function(record, json, relationship) {
    var key = relationship.key;

    if (key === 'items') {
      var serializer = Ember.get(record, 'store').serializerFor('item'),
          items = record.get('items'),
          serializedItems = [];

      if (items) {
        items.forEach(function(item) {
          var itemId = item.get('id');

          var serializedItem = serializer.serialize(item, {});

          if (itemId) {
            serializedItem.id = itemId;
          }

          serializedItems.push(serializedItem);
        });
      }

      json['items_attributes'] = serializedItems;
    }
    else {
      this._super.apply(this, arguments);
    }
  }
});

Does that help any?

I’m not having a problem serializing the models, the issue is when the response contains the relationships models and they are in an isDirty state then those dirty properties aren’t updated from the response or the objects marked as committed/saved.

Over a year since you posted this, and I’m still noticing the same problem. In general, I’ve found the process of dealing with nested attributes (in the Rails sense) hackish at best, even with the latest versions of Ember Data and ActiveRecordSerializer. Have you come across any better solutions since?

Sadly not, the same hacky workaround is in place.

Unfortunately, that hacky workaround didn’t even work for me! No errors; just never cleared the dirty flags.

Since my specific needs here were not for an arbitrary collection of nested objects, but nesting a single object with a finite number of fields, I wound up changing my implementation on the Rails side to pull them together into basically a Form Object, and serialize that:

Here’s an example of the Rails side. Doing it this way, the Ember side is straight-forward.

class Thing < ActiveRecord::Base
  has_one :other_thing
end

class OtherThing < ActiveRecord::Base
  belongs_to :thing
end

class Things
  include ActiveModel::Model
  include ActiveModel::Serialization

  attr_reader :thing, :other

  def initialize(thing)
    @thing = thing
    @other = thing.other_thing
  end
  
  delegate :id, :some_attr, :errors to: :thing
  delegate :other_attr, to: :other

  def submit(params)
    thing.attributes = params.slice(:some_attr)
    other.attributes = params.slice(:other_attr)

    if thing.valid? && other.valid?
      thing.save
    end
  end
end

def ThingSerializer < ActiveModel::Serializer
  attributes :id, :some_attr, :other_attr
end

def ThingsController < ApplicationController
  before_filter :set_thing, only: [:show, :update]

  def show
    render json: ThingSerializer.new(@thing).as_json
  end

  def update
    if @thing.submit(thing_params)
      render json: ThingSerializer.new(@thing).as_json
    else
      render json: { errors: @thing.errors }, status: :unprocessable_entity
    end 
  end
end