Deleting associated hasMany records in a nested route


#1

I’m trying to figure out the best Ember 2 way to display a list of associated records in a nested route for a single given record of a different type, and to be able to modify the list (by adding or deleting associated records) so that the template updates to show the latest list. For now I’m just concentrating on deleting associated records.

As a concrete example of what I have so far, say an author has many books and they are displayed in a nested route, /authors/:author_id/books:

// routes/authors/books.js
import Ember from 'ember'

export default Ember.route.extend({
  
  model () {
    const author = this.modelFor('authors')
    return Ember.get(author, 'books').sortBy('title')
  },
  
  actions: {
    deleteBook (book_id) {
      const author = this.modelFor('authors')
      const filteredBooks = Ember.get(author, 'books').filter(
        book => Ember.get(book, 'id') === book_id
      )
      if (filteredBooks.length < 1) { return }

      // At this point just delete locally in client app.
      filteredBooks[0].deleteRecord()
      console.log(Ember.get(filteredBooks[0], 'isDeleted')) // true
    }
  }
  
}

An author’s books are displayed by a template:

// templates/authors/books.hbs
<ul>
  {{#each model as |book|}}
    <li>
      <button {{action 'deleteBook' book.id}}>🗑</button>
      {{book.title}}
    </li>
  {{/each}}
</ul>

This works as far as displaying the initial list of books in the template, finding the correct book in the deleteBook method, and calling deleteRecord on it. Afterward, the isDeleted proeprty of the book returns true.

However, the list of books rendered in the template doesn’t change, and repeated clicks on the delete button for a single book continue to find and re-“delete” the book on every click. Can anyone shed any light on why this isn’t working as I expect? Is this a decent approach, or is there a different way I should be using Ember to accomplish the desired outcome?


#2

Hey @munderwood,

I would first recommend that you handle that action in your controller instead of your route. Then you don’t need to use ‘modelFor’ and can just reference ‘model’ directly. I like to think of the route as what fetches the data and handles any transition and associated state, and the controller handles user interaction and holds the GUi state, more or less.

Second, I think part of the problem may be that you’re calling deleteRecord and you’re never persisting that change via <model>.save(). Alternatively you can use destroyRecord instead of deleteRecord, which is essentially just a shortcut to do a deleteRecord and a save at the same time. See the guide for more details.

EDIT: a third thing you could do is actually pass the entire book in your action (instead of ‘book.id’) and just delete it that way instead of having to do a filter. Also, instead of doing filter you could use some of the Ember helpers and say:

    filteredBooks = Ember.get(author, 'books').findBy('id', book_id);

It’s a little cleaner.


#3

Hi @dknutsen,

Thanks for the reply and feedback. I’ve moved the action to the controller and changed it to accept the book instance instead of the book_id. The code is definitely a lot cleaner now!

Unfortunately, I don’t think you addressed the root issue that I’m having. As I pointed out in a comment on the call to deleteRecord in the sample code, my goal here is just to remove the associated record locally in the client.

I understand the difference between deleteRecord and destroyRecord, but I’m trying to let the user add and delete as many books as they want in the browser, and then either confirm the changes and save the new set in a single request to the server, or cancel the changes and leave the state on the server unchanged.

What I don’t understand is why deleting the record in the client doesn’t remove it from the list rendered by the template in the #each model block, regardless of the state on the server. Do changes to the state of the store not get propagated to the model on the route?


#4

I also just edited the calls to modelFor in the sample code of the question.

They are now getting the model of the parent route (books) first via modelFor, and then returning the child records of the hasMany association (authors) as the model for the nested route, /books/:book_id/authors. Now that I’ve corrected my error, do you still think that I should be able to ditch the modelFor call and instead reference model directly? If so, what would that look like?


#5

@munderwood sorry I misunderstood that the first time, my bad! I think what you’re seeing is an intentional design decision. If you do a “local” deleteRecord, the record is still in the store and still in the backend, it’s just been flagged for removal on the next save. So I think it makes sense to keep it in the live record array. This is somewhat speculative on my part, but I’m guessing the reason it works this way is so you could do a local delete and when the ‘isDeleted’ flag is set to true on the record either grey it out or hide it, depending on your use case. Then when you persist that delete to the backend the recordArray will update. So, for your use case, if you want to confirm the changes as you described, I would simply add an if/unless block to only draw the component if ‘isDeleted’ is false.

In terms of your most recent question, if I’m understanding what you’re asking correctly, I would do something along these lines:

// templates/authors/books.hbs
<ul>
  {{#each model as |book|}}
    {{#unless book.isDeleted}}
    <li>
      <button {{action 'deleteBook' book}}>🗑</button>
      {{book.title}}
    </li>
    {{/unless}}
  {{/each}}
</ul>
// routes/authors/books.js
import Ember from 'ember'

export default Ember.route.extend({
  
  model () {
    const author = this.modelFor('authors')
    return Ember.get(author, 'books').sortBy('title')
  },  
}
// controllers/authors/books.js
import Ember from 'ember';
export default Ember.Controller.extend({
  actions: {
    deleteBook (book) {
      if (!book) { return }
      // At this point just delete locally in client app.
      book.deleteRecord()
      console.log(Ember.get(book, 'isDeleted')) // true
    }
  }
};

EDIT: if for whatever reason you must also do a ‘save’ on the parent model (I can’t remember if you have to or not… I don’t think you do but maybe…) you could just pass the full parent model to the child:

// templates/authors/books.hbs
<ul>
  {{#each sortedBooks as |book|}}
    {{#unless book.isDeleted}}
    <li>
      <button {{action 'deleteBook' book}}>🗑</button>
      {{book.title}}
    </li>
    {{/unless}}
  {{/each}}
</ul>
// routes/authors/books.js
import Ember from 'ember'

export default Ember.route.extend({
  
  model () {
    return this.modelFor('authors')
  },  
}
// controllers/authors/books.js
import Ember from 'ember';
export default Ember.Controller.extend({
  // add a couple CPs for convenience
  author: Ember.computed('model', function(){
    return this.get('model');
  },
  books: Ember.computed('model.books', function(){
    return this.get('model.books');
  },
  // this will sort the books
  sortedBooks: Ember.computed.sort('books', 'bookSortDesc'),
  bookSortDesc: ['title'],
  actions: {
    deleteBook (book) {
      if (!book) { return }
      // At this point just delete locally in client app.
      book.deleteRecord()
      console.log(Ember.get(book, 'isDeleted')) // true
    }
  }
};

#6

Excellent, thank you! In particular the simple #unless block solved the one very specific aspect of the problem that originally prompted me to post the question. But it’s so helpful to see the complete worked example you gave, and to learn why I should use that as the solution. Now that you point it out, it does of course make sense that the deleted (but not destroyed) record has to still be accessible somehow, and there’s not really any better place than in the same array of records, with the isDeleted flag set.


#7

Awesome! Glad that worked. There’s all kinds of cool stuff you can do with Ember that can make your code shorter and faster and well-structured. I feel like I’m still learning new techniques and features all the time. In case you’re interested, here are a couple really cool resources I’ve run across recently: