Search code examples
javascriptember.jsember-dataember-drag-sort

ember-drag-sort: how to determine the new parent in the dragged list?


Originally asked by @andrewsyd on GitHub.

For a nested list, is there a way to determine in the dragEnd action which "sub-list" the item has been dropped into? (e.g. a class name, id etc)

In my scenario, I am sorting ember data records that can belong to each other (i.e. a nested, 'tree' structure). When I drag one nested record "into" another (making the dragged record a child of the second record), I need to update the parent attribute in ember-data. My question is, how do you pass some id of the second record (the new parent) to the dragEnd action?

Is this even possible?


Solution

  • Solution 0: adjust your data model to convert the isParent attribute into a derived value rather than a source of truth

    Having an isParent attribute that must be updated by hand is a flawed approach in the first place.

    If you have the isParent state as an attribute and require the frontend to update it, then you have two sources of truth that can (and eventually will) go out of sync. Especially so given the fact that users can tamper with network requests to your API backend.

    The isParent should be inferred from the amount of children. It could be a simple computed property:

    {
      isParent: computed('children.[]', function () {
        return this.get('children.length') > 0
      }
    }
    

    A similar approach can be used on the backend.

    If you don't control the backend and still need to update the isParent attribute from the frontend side, I recommend that you hack your serializer to include the isParent computed property value into the payload during serialization.

    Though I strongly believe you should go with this solution, I've researched a couple alternative solutions below.

    Solution 1: use an observer to update the parent state automatically

    In your model:

    {
      updateParentState: Ember.observer('children.[]', function () {
       const isParent = this.get('children.length') > 0
        this.setProperties({isParent})
      })
    }
    

    This will keep the isParent attribute synchronized with its children relationship whenever it's updated.

    Here's a demo: https://ember-twiddle.com/f1c737d3bc106cb9cca071fd01fe334f?openFiles=models.item.js%2C

    Note that if you automatically save your record(s) on drag end, you should wrap saving into Ember.run.next, so that saving happens after the observer fires.

    Solution 2: access the old and new parent of the dragged item

    Given that you have relationships set up like this:

    export default Model.extend({
      isParent: attr('boolean'),  
      parent: belongsTo('item', {inverse: 'children'}),
      children: hasMany('item', {inverse: 'parent'}),
    })
    

    ...you can access the old and new parent of the dragged item in the drag end action!

    {
      actions : {
        dragEnd ({sourceList, sourceIndex, targetList, targetIndex}) {
          if (sourceList === targetList && sourceIndex === targetIndex) return
    
          const draggedItem = sourceList.objectAt(sourceIndex)
          const oldParent = draggedItem.get('parent')                     // <--
    
          sourceList.removeAt(sourceIndex)
          targetList.insertAt(targetIndex, draggedItem)
    
          const newParent = draggedItem.get('parent')                     // <--
    
          newParent.set('isParent', newParent.get('children.length') > 0) // <--
          oldParent.set('isParent', oldParent.get('children.length') > 0) // <--
        },
      }
    }
    

    I've marked relevant lines with arrow comments.

    See, you read the old parent from the dragged item before moving it. After you move the item, you read the new parent. This is possible because Ember Data performs relationship bookkeeping automatically.

    Finally, you update the isParent state of both parents.

    Demo: https://ember-twiddle.com/ab0bfdce6a1f5ad4bd0d1c9c45f642fe?openFiles=controllers.application.js%2Ctemplates.components.the-item.hbs