snowYetis
snowYetis

Reputation: 1627

Ember Data 2 - Submit Two Associated Models in One Form

Total Ember newb here. My back-end is written with Rails. That piece is fine, so assume all is well on the server side. Here are the specifics:

I have two models Project (Has Many Project Details) Project Details (Belongs To Project)

I have confirmed that:

  1. I am able to create a new Project from my projects/new.hbs template
  2. My Index route retrieves all Projects and the Project Details associated with them.
  3. My Show route retrieves an individual Project and the Project Details associated with them.

What I would like to do is, submit a project model along with a project detail model from my projects/new.hbs template.

I understand that the form in new.hbs should be in a component but I am keeping it simple for now. Once, I am able to create my associated models I will work on doing the same as a form component.

Project Model

export default DS.Model.extend({
 project_details: DS.hasMany('project_detail', { async: true }),

 project_name: DS.attr('string'),
 status: DS.attr('string')
});

Project Detail Model

export default DS.Model.extend({
 project: DS.belongsTo('project', { async: true }),

 project_id: DS.attr('number'),
 feature_name: DS.attr('string'),
 hours_billed: DS.attr('number'),
 available_hours: DS.attr('number'),
 amout: DS.attr('number')
});

Projects/New.js Route

export default Ember.Route.extend({
  model: function() {
    return Ember.RSVP.hash({
      projects: this.store.createRecord('project'),
      project_details: this.store.createRecord('project_detail')
    });
  },
 actions: {
  create: function() {
    var self = this;
    this.controller.get('model').save().then(
    function() {
      self.transitionTo('projects.index');
    });
  }
 }
});

Projects/New.hbs

<form {{ action "create" on="submit" }}>
  <label for="project_name">Project Name</label>
  {{ input value=project_name }}
  <label for="status">Status</label>
  {{ input value=status }}
  <label for="feature_name">Feature Name</label>
  {{ input value=feature_name }}
  <label for="available_hours">Available Hours</label>
  {{ input value=available_hours }}
  <label for="hours_billed">Hours Billed</label>
  {{ input value=hours_billed }}
  <label for="amount">Amount</label>
  {{ input value=amount }}
  <button type="submit">Save</button>
</form>

Router.js

Router.map(function() {
  this.route('home');
  this.route('dashboard');
  this.route('login');
  this.resource('projects', function() {
    this.route('new');
    this.route('show', {path: '/:id'});
    this.route('edit', {path: '/:id/edit'});
  });
});

When I submit the form, the action appears to be called, but all data that is submitted is NULL, so evidently, my inputs are not being recognized. I have searched for a couple days and have not seen a solution for submitting multiple models. My code is a result of expanding on a standard POST along with how I handled retrieving my associated models via the RSVP Hash. Any advice would be great. Thanks!

Upvotes: 1

Views: 433

Answers (1)

Pedro Rio
Pedro Rio

Reputation: 1444

You've got some things that are not wired up correctly.

When you return the following from your route:

model: function() {
  return Ember.RSVP.hash({
    projects: this.store.createRecord('project'),
    project_details: this.store.createRecord('project_detail')
  });
},

In your template, you can use {{model.something}} to access that information, in your case you have access to {{model.projects}} and {{model.project_details}}.

You probably want to do the following in your route to setup easier access to your models in the templates.

setupController(controller, model) {
  //this._super(controller, model) I'll explain why this is commented
  controller.set('project', model.projects);
  controller.set('project_details', model.project_details); 
}

If you do this, then in your template you can do things like this:

<label>Project Name</label>
{{input value=project.project_name}}
{{project.project_name}}

Which brings to the next thing. When you have in your template the following:

{{ input value=project_name }}

What you're doing is binding the input value to the project_name property of your controller. If you use the Ember Inspector (hint, if you're not using it, you should. It makes life so much easier) you can probably check that the controller has that property project_name with the value you typed.

Before Ember 2.0 there was this thing called the Object Controller (which you don't use anymore) that allowed proxying the model to the controller. Which meant that you could on your route do this:

model() {
   return this.store.createRecord('project');
}

And in your template you effectively to this:

<label>Project Name</label>
{{project_name}}

Today you have to do this (Unless you install ember-legacy-controllers which I don't recommend):

<label>Project Name</label>
{{model.project_name}}

As you can see, you need to add the model before the project_name. This is because the model returned from the route is set as a property of the controller by default. That's something we can change, of course. You could also do the following in your route:

setupController(controller, model) {
  controller.set('model', model.projects);
  controller.set('myProjectDetails', model.project_details);
}

And in the template you could now do the following:

<form {{ action "create" on="submit" }}>
 <label for="project_name">Project Name</label>
 {{ input value=model.project_name }}
 <label for="status">Status</label>
 {{ input value=model.status }}
 <label for="feature_name">Feature Name</label>
 {{ input value=myProjectDetails.feature_name }}
 <label for="available_hours">Available Hours</label>
 {{ input value=myProjectDetails.available_hours }}
 <label for="hours_billed">Hours Billed</label>
 {{ input value=myProjectDetails.hours_billed }}
 <label for="amount">Amount</label>
 {{ input value=amount }}
 <button type="submit">Save</button>
</form>

Which means that your save function now works because the "project" is now your model. If you used my example of setupController where I did this:

controller.set('project', model.projects);

Then you would have to change your create action to this:

actions: {
  create: function() {
    var self = this;
    this.controller.get('project').save().then(
    function() {
     self.transitionTo('projects.index');
    });
  }
}

Also, do notice that you have your models declared with

//Project
project_details: DS.hasMany('project_detail', { async: true }),
//Project Detail
project: DS.belongsTo('project', { async: true }),

But you're not setting the relations in the code you posted. If you want the models to be related, in your create action you would need something like this (or in any other place really). I'm assuming in the following snippet that my setupController example is in the route so that the project is in the controller as project and the project_details as project_details.

 create: function() {
 var self = this;
 var project = this.controller.get('project');
 var projectDetails = this.controller.get('project_details');
 project.get('project_details').pushObject(projectDetails);
 project_details.set('project', project);

 project.save().then(function() {
    self.transitionTo('projects.index');
 });
}

Assuming you're using Ember Data 2.0, there's also another problem with this. When you save the project it will only make a post request to save the project itself (it won't save the newly created project_details with it). At the moment JSONAPI(the standard Ember Data 2.0 by default uses) does not have a solution for updating multiple resources in one go (see this github issue). This means that you would have to save the project_details first and only then then project (which probably doesn't make sense looking at your models as it seems the project_details only exists under a project.)

It's the current state of affairs, as far as I know. I'm also looking for a good solution for this problem (I've hacked together solutions like only allowing to create a project_detail after the project is created, or having an extra attribute in the project with is a string of the serialized project_detail json and then have the backend to the things it needs)

Upvotes: 4

Related Questions