23

In short, I am trying to reproduce a basic version of jquery-ui's shopping cart demo: http://jqueryui.com/demos/droppable/shopping-cart.html with ember.js and HTML5 native drag n drop.

After previously trying to implement drag and drop with ember + jquery-ui and having difficulty using this solution: http://jsfiddle.net/Wu2cu/2/, I saw pangratz's HTML5 solution: http://jsfiddle.net/pangratz666/DYnNH/ and decided to give it a shot.

I have forked pangratz's jsfiddle, created a productsController and an addedToCartController that filters the productsController based on an isAdded property: http://jsfiddle.net/GU8N7/3/

That works fine, but then I get stuck when I try to use an #each iterator and append unique draggable views to each object in the iterator. I want to be able to drag each "product" object, and when it is dropped into the "shopping cart" area, set that object's isAdded property to true, thus having it show up in the "shopping cart."

Any help would be greatly appreciated!!

(also as a bonus, I'd like to make the items in the shopping cart sortable, but that's probably too much to ask for until the first bridge is crossed.)

Jack Johnson
  • 704
  • 1
  • 6
  • 14

2 Answers2

47

Take a look at the code below for a solution (with a few extras). Sorting of the cart items is included (see cartController at the end of the JS).

And a working fiddle here: http://jsfiddle.net/ud3323/5uX9H/.

UPDATE: Added a drag image example.

Handlebars

<script type="text/x-handlebars" >  
    <b>Available Products</b>
    <br /><br />
    {{#each App.productsController}}
      {{#view App.ProductView contentBinding="this"}}
        {{content.name}}
      {{/view}}<br />
    {{/each}}
    <hr />

    {{#view App.ProductDropTarget 
            dragContextBinding="App.productsController.currentDragItem"}}
    Shopping Cart
    <div style="height: 20px">{{helpText}}</div>
    {{/view}}
    <br />
    {{#each App.cartController}}
      {{#view App.ProductView contentBinding="this"}}
        {{content.name}}
      {{/view}}<br />
    {{/each}}    
</script>​

JavaScript:

App = Ember.Application.create({});

DragNDrop = Ember.Namespace.create();

DragNDrop.cancel = function(event) {
    event.preventDefault();
    return false;
};

DragNDrop.Draggable = Ember.Mixin.create({
    attributeBindings: 'draggable',
    draggable: 'true',
    dragStart: function(event) {
        var dataTransfer = event.originalEvent.dataTransfer;
        dataTransfer.setData('Text', this.get('elementId'));
    }
});

DragNDrop.Droppable = Ember.Mixin.create({
    dragEnter: DragNDrop.cancel,
    dragOver: DragNDrop.cancel,
    drop: function(event) {
        event.preventDefault();
        return false;
    }
});

App.Product = Ember.Object.extend({
    name: null,
    isAdded: null
});

App.ProductView = Ember.View.extend(DragNDrop.Draggable, {
    tagName: 'span',

    // .setDragImage (in #dragStart) requires an HTML element as the first argument
    // so you must tell Ember to create the view and it's element and then get the 
    // HTML representation of that element.
    dragIconElement: Ember.View.create({
        attributeBindings: ['src'],
        tagName: 'img',
        src: 'http://twitter.com/api/users/profile_image/twitter'
    }).createElement().get('element'),

    dragStart: function(event) {
        this._super(event);
        // Let the controller know this view is dragging
        this.setPath('content.isDragging', true);

        // Set the drag image and location relative to the mouse/touch event
        var dataTransfer = event.originalEvent.dataTransfer;
        dataTransfer.setDragImage(this.get('dragIconElement'), 24, 24);
    },

    dragEnd: function(event) {
        // Let the controller know this view is done dragging
        this.setPath('content.isDragging', false);
    }
});

App.ProductDropTarget = Ember.View.extend(DragNDrop.Droppable, {
    tagName: 'div',
    classNames: ['dropTarget'],
    classNameBindings: ['cartAction'],
    helpText: null,

    // This will determine which class (if any) you should add to
    // the view when you are in the process of dragging an item.
    cartAction: Ember.computed(function(key, value) {
        if(Ember.empty(this.get('dragContext'))) {
            this.set('helpText','(Drop Zone)');
            return null;
        }

        if(!this.getPath('dragContext.isAdded')) {
            this.set('helpText', '(Drop to Add)');
            return 'cart-add';
        } else if(this.getPath('dragContext.isAdded')) {
            this.set('helpText', '(Drop to Remove)');
            return 'cart-remove';
        } else {
            this.set('helpText', '(Drop Zone)');
            return null;
        }

    }).property('dragContext').cacheable(),

    drop: function(event) {
        var viewId = event.originalEvent.dataTransfer.getData('Text'),
            view = Ember.View.views[viewId];

        // Set view properties
        // Must be within `Ember.run.next` to always work
        Ember.run.next(this, function() {
            view.setPath('content.isAdded', !view.getPath('content.isAdded'));
        });

        return this._super(event);
    }
});

App.productsController = Ember.ArrayController.create({
    content: [
      App.Product.create({ name: "MacBook Pro", isAdded: false }),
      App.Product.create({ name: "iPhone", isAdded: false }),
      App.Product.create({ name: "iPad", isAdded: true }),
      App.Product.create({ name: "iTV", isAdded: false })
    ],

    currentDragItem: Ember.computed(function(key, value) {
        return this.findProperty('isDragging', true);
    }).property('@each.isDragging').cacheable(),

    productsInCart: Ember.computed(function(key, value) {
        return this.filterProperty('isAdded', true);
    }).property('@each.isAdded').cacheable()

});

App.cartController = Ember.ArrayController.create({    
    content: Ember.computed(function(key, value) {
        var cartItems = this.get('cartItems');

        if(!Ember.empty(cartItems)) {
            // Sort desc by name
            return cartItems.sort(function(a,b){
                if((a.get('name').toLowerCase()) < (b.get('name').toLowerCase()))
                    return -1;
                else return 1;
            });
        }
    }).property('cartItems').cacheable(),

    cartItemsBinding: 'App.productsController.productsInCart'
});

​ ​

Roy Daniels
  • 6,309
  • 2
  • 27
  • 29
  • 3
    Thanks so much man, that is awesome! You went above and beyond in answering the question. Many many thanks for your help! I'm going to be implementing this with dragging clones of product photos - is there any easy way to implement clone dragging in this demo? Once again, thanks so much man, you kick a lot of ass. – Jack Johnson May 27 '12 at 08:25
  • Here's a JSBin of a simple example based on yours using components http://emberjs.jsbin.com/erETonuR/2/edit – knownasilya Jan 10 '14 at 20:59
  • 1
    Heads up for anyone using this example, `setPath` and `getPath` have been removed in favor of `set` and `get`. https://github.com/emberjs/ember.js/pull/1176/files – Greg W Jan 24 '14 at 14:58
2

i was looking for a drag n drop exemple and find yours, i updated the code slightly to 1.0.0-rc5 and add a double click on item ability for fun ...

http://jsfiddle.net/kadactivity/hhBrM/1/

Handlebars

<script type="text/x-handlebars" >  
    <b>Available Products</b>
    <br /><br />
    {{#each product in model}}
        {{#view App.ProductView contentBinding="product"}}
            {{view.content.name}}
        {{/view}}<br />
    {{/each}}
    <hr />

    {{#view App.ProductDropTarget 
        dragContextBinding="currentDragItem"}}
    Shopping Cart
    <div style="height: 20px">{{helpText}}</div>
    {{/view}}
    <br />
    {{#each cart in productsInCart}}
        {{#view App.ProductView contentBinding="cart"}}
            {{view.content.name}}
        {{/view}}<br />
    {{/each}}    
</script>

Javascript

App = Ember.Application.create();

App.Router.map(function() {
  // put your routes here
});

App.ApplicationRoute = Ember.Route.extend({
  model: function() {
    return [
      App.Product.create({ name: "MacBook Pro", isAdded: false }),
      App.Product.create({ name: "iPhone", isAdded: false }),
      App.Product.create({ name: "iPad", isAdded: true }),
      App.Product.create({ name: "iTV", isAdded: false })
    ];
  }
});

DragNDrop = Ember.Namespace.create();

DragNDrop.cancel = function(event) {
  event.preventDefault();
  return false;
};

DragNDrop.Draggable = Ember.Mixin.create({
  attributeBindings: "draggable",
  draggable: "true",
  dragStart: function(event) {
    var dataTransfer = event.originalEvent.dataTransfer;
    dataTransfer.setData("Text", this.get("elementId"));
  }
});

DragNDrop.Droppable = Ember.Mixin.create({
  dragEnter: DragNDrop.cancel,
  dragOver: DragNDrop.cancel,
  drop: function(event) {
    event.preventDefault();
    return false;
  }
});

App.Product = Ember.Object.extend({
  name: null,
  isAdded: null
});

App.ProductView = Ember.View.extend(DragNDrop.Draggable, {
  tagName: "span",

  // .setDragImage (in #dragStart) requires an HTML element as the first argument
  // so you must tell Ember to create the view and it"s element and then get the 
  // HTML representation of that element.
  dragIconElement: Ember.View.create({
    attributeBindings: ["src"],
    tagName: "img",
    src: "http://twitter.com/api/users/profile_image/twitter"
  }).createElement().get("element"),

  dragStart: function(event) {
    this._super(event);
    // Let the controller know this view is dragging
    this.set("content.isDragging", true);

    // Set the drag image and location relative to the mouse/touch event
    var dataTransfer = event.originalEvent.dataTransfer;
    dataTransfer.setDragImage(this.get("dragIconElement"), 24, 24);
  },

  dragEnd: function(event) {
    // Let the controller know this view is done dragging
    this.set("content.isDragging", false);
  },

  doubleClick: function(event) {
    this.set("content.isAdded", !this.get("content.isAdded"));
  }
});

App.ProductDropTarget = Ember.View.extend(DragNDrop.Droppable, {
  tagName: "div",
  classNames: ["dropTarget"],
  classNameBindings: ["cartAction"],
  helpText: null,

  // This will determine which class (if any) you should add to
  // the view when you are in the process of dragging an item.
  cartAction: function() {
    if(Ember.isEmpty(this.get("dragContext"))) {
        this.set("helpText","(Drop Zone)");
        return null;
    }

    if(!this.get("dragContext.isAdded")) {
        this.set("helpText", "(Drop to Add)");
        return "cart-add";
    } else if(this.get("dragContext.isAdded")) {
        this.set("helpText", "(Drop to Remove)");
        return "cart-remove";
    } else {
        this.set("helpText", "(Drop Zone)");
        return null;
    }

  }.property("dragContext"),

  drop: function(event) {
    var viewId = event.originalEvent.dataTransfer.getData("Text"),
        view = Ember.View.views[viewId];

    // Set view properties
    // Must be within `Ember.run.next` to always work
    Ember.run.next(this, function() {
        view.set("content.isAdded", !view.get("content.isAdded"));
    });

    return this._super(event);
  }
});

App.ApplicationController = Ember.ArrayController.extend({
  currentDragItem: function() {
      return this.findProperty("isDragging", true);
  }.property("@each.isDragging"),

  productsInCart: function() {
    var cartItems = this.filterProperty("isAdded", true);
    console.log(cartItems);
    if(!Ember.isEmpty(cartItems)) {
      // Sort desc by name
      return cartItems.sort(function(a,b){
          if((a.get("name").toLowerCase()) < (b.get("name").toLowerCase()))
              return -1;
          else return 1;
      });
    }
  }.property("@each.isAdded")
});