Ember-Data beta 3 and saving hasMany relationships with ActiveModelSerializer and Rails


#1

Over the past few hours I’ve been fighting against ember-data to save a many-to-many relationship with ActiveModelSerializer to my Rails 4 app. I’ve gotten it to work after browsing through a number of other peoples code and seeing what they have done. This issue has been talked about a ton it seems, but I still had to struggle a bit to get my case to work. So I thought I’d share what I ended up with to see if it’s correct and possibly to help others who’re having a difficult time.

Here is the code I ended up with:

store.js

Sis.ApplicationAdapter = DS.ActiveModelAdapter.extend({});

Sis.ApplicationSerializer = DS.ActiveModelSerializer.extend({
  // This is solution for embedding entire objects into save post. 
  // I couldn't get this solution to save the relationships on the rails side due to strong parameters, 
  // so the below solution of including ids is what I went with.
  // 'attrs': {
  //   'students': { embedded: 'always'},
  // },

  // Below is solution to only include ids for saving hasMany relationships
  // I adapted this from here: http://dbushell.com/2013/04/25/ember-data-and-mongodb/
  serializeHasMany: function(record, json, relationship) {
    var key = relationship.key,
        jsonKey = Ember.String.singularize(key) + '_ids';
    json[jsonKey] = [];
    record.get(key).forEach(function(item) {
        json[jsonKey].push(item.get('id'));
    });
    return json;
  }
});

new_task_controller.js

Sis.NewTaskController = Ember.ObjectController.extend({
  needs: ["requiredTasks", "project"],
  isShowingUsers: false,
  actions: {
    createNewSubtask: function(requiredTask) {
      var content = this.get('content'),
          projectController = this.get('controllers.project'),
          students = content.get('students');
      // TODO: I'm not sure if this is needed or if ember-data will auto-associate the
      // many-to-many relationship on save. Keeping for now.
      students.forEach(function(student, index){
        student.get('tasks').addObject(content);
      });
      // Set the associated project, projectGroup, and parentTask
      content.set('project', projectController.get('model'));
      content.set('projectGroup', projectController.get('projectGroup'));
      content.set('parentTask', requiredTask);
      // Save the new subtask
      content.save();
    },
  }
});

new_task_view.js

Sis.NewTaskView = Ember.View.extend({
  content: function() {
    return this.controller.get('content');
  }.property('this.controller.content'),
  didInsertElement: function() {
    $('.datepicker').datepicker();
  },
  // Overrides Ember.Checkbox to add the checked student to this content
  userCheckbox: Em.Checkbox.extend({
    checkedObserver: function(){
      var content = this.get('controller').get('content')
      if(this.get('checked')) {
        content.get('students').pushObject(this.get('student'));
      } else {
        content.get('students').removeObject(this.get('student'));
      }
    }.observes('checked')
  }),
  actions: {
    addNewTask: function() {
      var content = this.get('content'), 
          requiredTask = this.get('parentView').get('controller').get('model');
      this.get('controller').send('createNewSubtask', requiredTask);
    },
    toggleUserSelect: function() {
      var controller = this.get('controller');
      controller.toggleProperty('isShowingUsers');
      if (controller.get('isShowingUsers')) {
        Ember.run.next(this, function() {
          // Setup the click handler to set isShowingUsers to false
          $('body').one('click', function() {
            controller.toggleProperty('isShowingUsers');
          });
          // Make sure the select-users-container doesn't trigger the above event
          $(".select-users-container").click(function(e) {
            e.stopPropagation();
            return true;
          });
        });
      }
    }
  },
})

newTask.handlebars

<form role="form" class="form-horizontal">
  <div class="form-group col-lg-10">
    {{view Ember.TextField valueBinding="title" placeholder="Title" required="true" class="form-control"}}
  </div>
  <div class="form-group col-lg-10">
    <label for="subtask-due-date" class="col-lg-2 control-label">Due Date:</label>
    <div id="new-task-date" class="datepicker input-append date col-lg-2" data-date-format="dd-mm-yyyy">
      {{view Sis.DateField valueBinding="dueDate" class="form-cntrol" size="15"}}
      <span class="add-on"><i class="icon-th"></i></span>
    </div>
    <label for="subtask-users" class="col-lg-3 control-label">Assigned to:</label>
    <a {{action toggleUserSelect target="view"}} {{bind-attr class="isShowingUsers:hide"}}>Select</a>
    {{#if isShowingUsers}}
      <div class="select-users-container">
        <ul id="select-users-list">
          {{#each student in controllers.requiredTasks.selectableStudents}}
            {{view view.userCheckbox studentBinding="student" class="user-select-checkbox"}}
            {{student.fullName}}
          {{/each}}
        </ul>
      </div>
    {{/if}}
  </div>
  <div class="col-lg-3">
    <button {{action addNewTask target="view"}} type="button" class="btn btn-default">Add Task</button>
    <button {{action cancelNewTask}} type="button" class="btn btn-default">Cancel</button>
  </div>
</form>

Relevant models

Sis.Student = Sis.User.extend({
  projectGroups: DS.hasMany('projectGroup'),
  tasks: DS.hasMany('subtask')
});

Sis.Subtask = Sis.Task.extend({
  projectGroup: DS.belongsTo('projectGroup'),
  parentTask: DS.belongsTo('requiredTask'),
  students: DS.hasMany('student')
});

Rails 4 strong_parameters to allow for saving ‘student_ids’ inside subtasks_controller.rb

def subtask_params
  params.require(:subtask).permit(:title, :type, :project_group_id, 
    :parent_task_id, :is_completed, {:student_ids => []})
end 

Any comments or suggestions welcome. This is the first time I’ve had to dig a bit deeper into ember/ember-data to get something working so I’m interested in getting some feedback. Thanks.