Ember data model callbacks and triggering events on associated models


#1

I have a best practice question. I currently have a mapping app which maps out routes based on where a user drops markers all using ember leaflet. It’s still in the early stages and I’d like to set off down the right road (no pun intended).

What I currently have is:

http://recordit.co/p2ktGKddxf

When a user drops a maker after dragging, the onDragend method persists the marker’s (waypoint) new position.

The marker has 2 associated polylines (route), the one coming into it (incomingRoute) and the one going out to the next marker (outgoingRoute) which also need to be updated to represent the new position.

Question is, currently, the updating of the route models happens in a controller action which is passed to a component to fire on onDragend of the marker. The problem here is it feels way too out of the remit of the controller to manage that functionality and also, you’ll note on the animation above, if I change the coords of the waypoint in the sidebar, it obviously doesn’t fire the update as it’s part of the drag end code.

In rails I’d likely do something on the after_save hook that triggered the routes to do the work themselves:

def some_after_save_method do
    self.incoming_route.some_update_method
    self.outgoing_route.some_update_method
end

I was wondering what the idiomatic ember approach was to making route updates the concern of route and not the controller action?

I currently have a crude, procedural action on a controller which is working but I’d like to understand and refactor it to the proper ‘ember way’ of doing it to allow for validation/save failures.

What I currently have is:

// Waypoint Model

export default DS.Model.extend({
  incomingRoute: DS.belongsTo('route', {inverse: 'waypointEnd'}),
  outgoingRoute: DS.belongsTo('route', {inverse: 'waypointStart'}),
  ride: DS.belongsTo('ride'),
  name: DS.attr('string'),
  latlng: DS.attr('point-to-lat-lng'),
  position: DS.attr('number')
});

// Route Model

export default DS.Model.extend({
  waypointStart: DS.belongsTo('waypoint', {inverse: 'outgoingRoute'}),
  waypointEnd: DS.belongsTo('waypoint', {inverse: 'incomingRoute'}),
  ride: DS.belongsTo('ride'),
  line: DS.attr('line_string_to_polyline'),
  state: DS.attr('string')
});

// Ride Controller

export default Controller.extend({
  ...

  actions: {
    ...

    async markerDragend(waypoint, loc){
      waypoint.get('incomingRoute').then(function(r){
        if (r) {
          let line = r.get('line');
          line.pop();
          line.push([String(loc.lat), String(loc.lng)]);
          r.setProperties({
            line: A(line),
            stateColour: '#3388ff',
            stateOpacity: 1
          });
          r.save();
        }
      });
      waypoint.get('outgoingRoute').then(function(r){
        if (r) {
          let line = r.get('line');
          line.shift();
          line.unshift([String(loc.lat), String(loc.lng)]);
          r.setProperties({
            line: A(line),
            stateColour: '#3388ff',
            stateOpacity: 1
          });
          r.save();
        }
      });
      waypoint.set('latlng', loc);
      await waypoint.save();
    },
    ...

  }
});

Any input/steer you guys could offer would be much appreciated!

Andy.


#2

I have a feeling this is being overlooked because the answer is to use an observer but they tend to be frowned upon. Might that be the case?


#3

False alarm. I completely missed the events tab on the api docs. That’s solved half of it.


#4

We merged an RFC to deprecate events. I would do it in the action in the controller or the route.


#5

Oh right… what if multiple routes/controllers modify the model? Its the model changing that creates the need for the updates to be triggered so I’d have thought the model hooks/events would be the DRYest place for this to go?

Can you link me to the reasoning behind the removal so I can have a swot up?


#6

One approach that I’ve taken is to use a service.

In my case it was seat Reservations for event booking. My reservations Service is in charge of the logic around attaching seats and making sure that old reservations are cleaned up. This way, you can inject the service anywhere you need to use it, ie. your waypoint component and sidebar.

If you make sure that all dealings of waypoints go through your service, you can ensure that all the right processing will take place, without overloading your models, or duplicating logic in the rest of the app.

If you’re familiar with the Phoenix Framework this is similar to the concept of its Contexts, providing the public API for the model layer.


#7

A good workaround @geoffreyd but I would still be interested in hearing the reason for removing model hooks @jthoburn?

Coming from the rails ecosystem, there are plenty of legitimate use cases for them. I’d just be interested in hearing why the deprecation went through.


#8

There were many reasons, but one of the biggest is that automatic mutations without context on what triggered the change is an awful design pattern.

RFC https://github.com/emberjs/rfcs/blob/5a1e8cddd3c1a210c2d60c7a9ac021dbfbedc370/text/1234-deprecated-ember-evented-in-ember-data.md


#9

As @jthoburn said, context-less mutations can be unpredictable, and messy. Even in Rails, many developers consider model hooks to be an anti pattern to be avoided (we’ve pretty much banned them in-house). This is where services/contexts come into play, in both Rails and Ember, as an intentional design pattern, rather than a ‘workaround’.