javascriptember.jsember-datarsvp-promise

Ember Data belongsTo async relationship omitted from createRecord() save() serialization


Edit 11/16/14: Version Information

DEBUG: Ember      : 1.7.0 ember-1.7.0.js:14463
DEBUG: Ember Data : 1.0.0-beta.10+canary.30d6bf849b ember-1.7.0.js:14463
DEBUG: Handlebars : 1.1.2 ember-1.7.0.js:14463
DEBUG: jQuery     : 1.10.2 

I'm beating my head against a wall trying to do something that I think should be fairly straightforward with ember and ember-data, but I haven't had any luck so far.

Essentially, I want to use server data to populate a <select> dropdown menu. When the form is submitted, a model should be created based on the data the user chooses to select. The model is then saved with ember data and forwarded to the server with the following format:

{ 
    "File": { 
        "fileName":"the_name.txt",
        "filePath":"/the/path",
        "typeId": 13,
        "versionId": 2
    }
}

The problem is, the typeId and versionId are left out when the model relationship is defined as async like so:

App.File =  DS.Model.extend({
    type: DS.belongsTo('type', {async: true}),
    version: DS.belongsTo('version', {async: true}),
    fileName: DS.attr('string'),
    filePath: DS.attr('string')
});

The part that is confusing me, and probably where my mistakes lie, is the controller:

App.FilesNewController = Ember.ObjectController.extend({
    needs: ['files'],
    uploadError: false,

    // These properties will be given by the binding in the view to the 
    //<select> inputs.  
    selectedType: null,
    selectedVersion: null,

    files: Ember.computed.alias('controllers.files'),

    actions: {
        createFile: function() {
            this.createFileHelper();
        }
    },

    createFileHelper: function() {
        var selectedType = this.get('selectedType');
        var selectedVersion = this.get('selectedVersion');

        var file = this.store.createRecord('file', {
                fileName: 'the_name.txt',
                filePath: '/the/path'
        });

        var gotDependencies = function(values) {

            //////////////////////////////////////
            // This only works when async: false
            file.set('type', values[0])
                .set('version', values[1]);
            //////////////////////////////////////

            var onSuccess = function() {
                this.transitionToRoute('files');
            }.bind(this);

            var onFail = function() {
                this.set('uploadError', true);
            }.bind(this);

            file.save().then(onSuccess, onFail);
        }.bind(this);

        Ember.RSVP.all([
            selectedType,
            selectedVersion
        ]).then(gotDependencies);
    }
});

When async is set to false, ember handles createRecord().save() POST requests correctly.

When async is true, ember handles GET requests perfectly with multiple requests, but does NOT add the belongsTo relationships to the file JSON during createRecord().save(). Only the basic properties are serialized:

{"File":{"fileName":"the_name.txt","filePath":"/the/path"}}

I realize this question has been asked before but I have not found a satisfactory answer thus far and I have not found anything that suits my needs. So, how do I get the belongsTo relationship to serialize properly?

Just to be sure that everything is here, I will add the custom serialization I have so far:

App.ApplicationSerializer = DS.RESTSerializer.extend({
    serializeIntoHash: function(data, type, record, options) {
        var root = Ember.String.capitalize(type.typeKey);
        data[root] = this.serialize(record, options);
    },
    keyForRelationship: function(key, type){
        if (type === 'belongsTo') {
            key += "Id";
        }
        if (type === 'hasMany') {
            key += "Ids";
        }
        return key;
    }
});

App.FileSerializer = App.ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
    attrs: {
        type: { serialize: 'id' },
        version: { serialize: 'id' }
    }
});

And a select:

{{ view Ember.Select
    contentBinding="controller.files.versions"
    optionValuePath="content"
    optionLabelPath="content.versionStr"
    valueBinding="controller.selectedVersion"
    id="selectVersion"
    classNames="form-control"
    prompt="-- Select Version --"}}

If necessary I will append the other routes and controllers (FilesRoute, FilesController, VersionsRoute, TypesRoute)

EDIT 11/16/14

I have a working solution (hack?) that I found based on information in two relevant threads:

1) How should async belongsTo relationships be serialized?

2) Does async belongsTo support related model assignment?

Essentially, all I had to do was move the Ember.RSVP.all() to after a get() on the properties:

createFileHelper: function() {
    var selectedType = this.get('selectedType');
    var selectedVersion = this.get('selectedVersion');

    var file = this.store.createRecord('file', {
            fileName: 'the_name.txt',
            filePath: '/the/path',
            type: null,
            version: null
    });


    file.set('type', values[0])
        .set('version', values[1]);

    Ember.RSVP.all([
        file.get('type'),
        file.get('version')
    ]).then(function(values) {

        var onSuccess = function() {
            this.transitionToRoute('files');
        }.bind(this);

        var onFail = function() {
            alert("failure");
            this.set('uploadError', true);
        }.bind(this);

        file.save().then(onSuccess, onFail);
    }.bind(this));
}

So I needed to get() the properties that were belongsTo relationships before I save the model. I don't know is whether this is a bug or not. Maybe someone with more knowledge about emberjs can help shed some light on that.


Solution

  • See the question for more details, but the generic answer that I worked for me when saving a model with a belongsTo relationship (and you specifically need that relationship to be serialized) is to call .get() on the properties and then save() them in then().

    It boils down to this:

    var file = this.store.createRecord('file', {
            fileName: 'the_name.txt',
            filePath: '/the/path',
            type: null,
            version: null
    });
    
    // belongsTo set() here
    file.set('type', selectedType)
        .set('version', selectedVersion);
    
    Ember.RSVP.all([
        file.get('type'),
        file.get('version')
    ]).then(function(values) {
    
        var onSuccess = function() {
            this.transitionToRoute('files');
        }.bind(this);
    
        var onFail = function() {
            alert("failure");
            this.set('uploadError', true);
        }.bind(this);
    
        // Save inside then() after I call get() on promises
        file.save().then(onSuccess, onFail);
    
    }.bind(this));