0
votes

This is a similar question to this one, except this is for the latest versions of Ember and Active Model Serializers (0.10.2).

I have a simple Parent:Child relationship.

app/models/trail.js

import Ember from 'ember';
import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr(),

  // relationships
  employees: DS.hasMany('employee', { async: true }),

});

app/models/employee.js

import DS from 'ember-data';

import Person from '../models/person';

export default Person.extend({
  status: DS.attr(),
  statusCode: DS.attr(),
});

app/models/person.js

import Ember from 'ember';
import DS from 'ember-data';

export default DS.Model.extend({
  avatarUrl: DS.attr(),
  firstName: DS.attr(),
  lastName: DS.attr(),

  fullName: Ember.computed('firstName', 'lastName', function() {
    return `${this.get('lastName')}, ${this.get('firstName')}`;
  }),
});

When I create a new Trail, and select two employees for the 'hasMany', the following json arrives the server (from the Rails log):

{"data":
  {"attributes":
    {"name":"TEST3", 
      "gpx-file-url":"a url", 
      "distance-value":"5"}, 
      "relationships":
         {"employees":{"data":[]}}, "type":"trails"}}

My question is, what has happened to the employees? Where are the id's of the employees (they already exist both in the database and in the Ember Store - ie, I am not trying to create child records in this request).

EDIT

I just found this question, which explains that the id's for a hasMany relationship are not sent by Ember's JSONAPISerializer to the API - since the foreign key here actually has to be persisted in each child record. So essentially by 'selecting' employees, you need to save the fact that they now have a parent. So the selected employee records need to be persisted.

But my understanding was that this all works "out of the box" and that Ember would automatically fire a POST request to do this, but that seems to not be the case.

This then gets to the real question - how do I update those children?


UPDATE - BOUNTY ADDED AS THIS HAS QUESTION HAS EVOLVED


After further analysis, it became clear that a new model was required - Assignments. So now the problem is more complex.

Model structure is now this:


Trail

hasMany assignments


Employee

hasMany assignments


Assignment

belongsTo Trail

belongsTo Employee


In my 'new Trail' route, I use the fantastic ember-power-select to let the user select employees. On clicking 'save' I plan to iterate through the selected employees and then create the assignment records (and obviously save them, either before or after saving the Trail itself, not sure which is best yet).

The problem is still, however, that I don't know how to do that - how to get at the 'selected' employees and then iterate through them to create the assignments.

So, here is the relevant EPS usage in my template:

in /app/templates/trails/new.hbs

  {{#power-select-multiple options=model.currentEmployees 
    searchPlaceholder="Type a name to search"
    searchField="fullName"
    selected=staff placeholder="Select team member(s)"
    onchange=(route-action 'staffSelected') as |employee| 
  }}
  <block here template to display various employee data, not just 'fullName'/>
  {{/power-select-multiple}}

(route-action is a helper from Dockyard that just automatically sends the action to my route, works great)

Here is my model:

  model: function () {
    let myFilter = {};
    myFilter.data = { filter: {status: [2,3] } }; // current employees
    return Ember.RSVP.hash({
      trail: this.store.createRecord('trail'),
      currentEmployees: this.store.query('employee', myFilter).then(function(data) {return data}),
    });
  },

  actions: {
    staffSelected (employee) {
      this.controller.get('staff').pushObject(employee);      
      console.log(this.controller.get('staff').length);
    },
  }

I only discovered today that we still need controllers, so this could be my problem! Here it is:

import Ember from 'ember';

export default Ember.Controller.extend({
  staff: [] <- I guess this needs to be something more complicated
});

This works and I see one object is added to the array in the console. But then the EPS refuses to work because I get this error in the console:

trekclient.js:91 Uncaught TypeError: Cannot read property 'toString' of undefined(anonymous function) @ trekclient.js:91ComputedPropertyPrototype.get @ vendor.js:29285get @ 

etc....

Which is immediately follow by this:

vendor.js:16695 DEPRECATION: You modified (-join-classes (-normalize-class "concatenatedTriggerClasses" concatenatedTriggerClasses) "ember-view" "ember-basic-dropdown-trigger" (-normalize-class "inPlaceClass" inPlaceClass activeClass=undefined inactiveClass=undefined) (-normalize-class "hPositionClass" hPositionClass activeClass=undefined inactiveClass=undefined) (-normalize-class "vPositionClass" vPositionClass activeClass=undefined inactiveClass=undefined)) twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 3.0 [deprecation id: ember-views.render-double-modify]

So I imagine this is because the examples in the documentation just uses an array containing strings, not actual Ember.Objects. But I have no clue how to solve this.

So, I decided to throw away the controller (ha ha) and get creative.

What if I added a property to the Trail model? This property can basically be a 'dummy' property that collected the selected employees.

in /app/models/trail.js

selectedEmps: DS.hasMany('employee', async {false}) 

I set async to false since we will not persist them and before saving the new Trail I can just set this to null again.

in /app/templates/trails/new.js

  {{#power-select-multiple options=model.currentEmployees 
    searchPlaceholder="Type a name to search"
    searchField="fullName"
    selected=model.selectedEmps placeholder="Select team member(s)"
    onchange=(action (mut model.selectedEmps)) as |employee| 
  }}
  <block here again/>
  {{/power-select-multiple}}

This works, it doesn't 'blow up' after selecting the first employee. I can select multiple and delete them from the template. The control seems to work fine, as it is mutating 'model.selectedEmps' directly.

Now, I think this is a hack because I have two problems with it:

  1. If I change the 'mut' to an action, so I can add further logic, I cannot figure out how to access what is actually stored in the propery 'model.selectedEmps'
  2. Even if I can figure out (1) I will have to always make sure that 'selectedEmps' is emptied when leaving this route, otherwise the next time this route is entered, it will remember what was selected before (since they are now in the Ember.Store)

The fundamental issue is that I can live with 'mut' but still have the problem that when the user hits 'Save' I have to figure out which employees were selected, so I can create the assignments for them.

But I cannot figure out how to access what is selected. Maybe something this Spaghetti-Monster-awful mess:

    save: function (newObj) {
      console.log(newObj.get('selectedEmps'));
      if (newObj.get('isValid')) {
        let emp = this.get('store').createRecord('assignment', {
          trail: newObj,
          person: newObj.get('selectedEmps')[0]
        })
        newObj.save().then( function (newTrail) {
            emp.save();
            //newTrail.get('selectedEmps')
 //         this.transitionTo('trails');
          console.log('DONE');
        }); 
      }
      else {
        alert("Not valid - please provide a name and a GPX file.");
      }
    },

So there are two problems to solve:

  1. How to get the selected employees, iterate and create the assignments.
  2. How to then save the results to the API (JSON-API using Rails). I presume that newObj.save and each assignment.save will take care of that.

UPDATE


The developer of EPS kindly pointed out that the action handler receives an array, since I changed to using a multiple select, not a single select as it had been earlier. So the action is receiving the full array of what is currently selected. DOH!

I was thus able to update the action handler as follows, which now successfully stores the currently selected employees in the staff property of the controller. One step closer.

  staffSelected(newList) {
      existing.forEach(function(me){
        if (!newList.includes(me)) {
          existing.removeObject(me); // if I exist but the newList doesn't have me, remove me
        }
      });
      newList.forEach(function(me){
        if (!existing.includes(me)) {
          existing.pushObject(me); // if I don't exist but the newList has me, add me
        }          
      });
  }

Perhaps not the best way to intersect 2 arrays but that's the least of my concerns at 4am on a Saturday night. :(


FINAL PROBLEM UPDATE - how to save the data?


Ok, so now that I can get the selected employees, I can create assignments, but still cannot figure out what Ember requires for me to save them, this save action throws an error:

save: function (newObject) {
      if (newObject.get('isValid')) {
        let theChosenOnes = this.controller.get('theChosenOnes');
        let _store = this.get('store');
        theChosenOnes.forEach(function (aChosenOne) {
          _store.createRecord('assignment', {
            trail: newObject,
            person: aChosenOne,
          });
        });
        newObject.save().then(function (newTrail) {
          newTrail.get('assignments').save().then(function() {
            console.log('DONE');
          });
        });
      }

get(...).save is not a function
1
Does the employee model have a belongsTo relationship with trail? If so, I believe that the record only gets stored in the belongsTo side by default as it's the norm when it comes to ORM.Mikko Paderes
No, there is no belongsTo defined. Should I add that?rmcsharry
@MikkoPaderes I added the 'belongsTo' and that results in the relationship array of employees being removed from the json being sent to api. That makes sense, since you would not normally expect the 'trail' create endpoint to receive this array and update the foreign keys of all those employees. Although I can imagine scenarios where you might want that.rmcsharry
yes there would be scenarios where you'd want that. For those cases you should override the serializer or create your own. See this if you're interested in customizing your serializer but I'd suggest you avoid it as much as possible as it'll be an extra burden in terms of maintenance.Mikko Paderes
Could you add the code where you select the two employees to be added to the trail? I think the problem lies in there, but I cannot say for sure until I see the code.physphun

1 Answers

2
votes

The problem with your final update is that in Ember Data 2.x, relationships are asynchronous by default, so what's returned from newTrail.get('assignments') is not a DS.ManyArray, which has a .save, but a PromiseArray, which doesn't have that.

You need a small tweak to do this instead, so you call .save on the resolved relationship:

newObject.save().then(function (newTrail) {
    newTrail.get('assignments').then(assignments => assignments.save()).then(function() {
        console.log('DONE');
    });
});