Dynamic Sortable Table using Backbone.js

by helgatheviking

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<div id="container">
<table id="my-table"></table>
</div>
<button id="add-product">Add</button>

<script type="text/template" id="products-table-header">
  <thead>
    <tr>
      <th>Product</th>
      <th>Quantity</th>
      <th>&nbsp;</th>
    </tr>
  </thead>
  <tbody></tbody>

</script>

<script type="text/template" id="product-row">

  <td class="product-title">
    <%= title %>
  </td>
  <td class="product-quantity">
    <input type="number" name="data[<%= product_id %>][quantity]" value="<%= quantity %>" />
  </td>
  <td class="product-remove">
    <a class="delete-product " href="#">Remove</a>
  </td>

</script>

CSS

table {
  border-collapse: collapse;
  margin-bottom: 1em;
}

th,
td {
  border: 1px solid black;
  font-size: 1em;
  font-family: Arial;
  padding: .25em;
}

input {
  width: 4em;
  text-align: center;
}

tr.ui-sortable-handle {
  cursor: move;
}

JavaScript

var myApplication = {};

(function($, myApplication) {

// Model
myApplication.Product = Backbone.Model.extend({
  defaults: {
    "quantity": 1,
    "title": "",
    "product_id": ""
  }
});

// Collection
myApplication.ProductsCollection = Backbone.Collection.extend({
  model: myApplication.Product,
  el: "#container"
});

// Singular Row View
myApplication.productView = Backbone.View.extend({

  model: myApplication.Product,
  tagName: 'tr',

  events: {
    'click .delete-product': 'removeProduct',
    'change .product-quantity :input': 'setQuantity'
  },

  // Get the template from the DOM.
  template: _.template($("#product-row").html()),

  // Render the single model.
  render: function() {
    this.$el.html(this.template(this.model.toJSON()));
    return this;
  },

  // Remove/destroy a model.
  removeProduct: function(e) {
    e.preventDefault();
    this.model.destroy();
    myApplication.productList.render();
  },

  // Persist the quantity in the model.
  setQuantity: function(e) {
    value = this.$el.find('.product-quantity :input').val();
    this.model.set('quantity', value);
  }

});

// List View
myApplication.productListTable = Backbone.View.extend({

  el: "#my-table",
  template: _.template($("#products-table-header").html()),

  addSingle: function(model) {
    var view = new productView({
      model: model
    });
    this.$el.find('tbody').prepend(view.render().el);
  },

  // Callback for SelectWoo sectionn
  addProduct: function(event, attributes) {
    Product = new myApplication.Product(attributes);
    this.collection.add(Product, {
      at: 0
    });
    this.render();
  },

  events: {
    'click .delete-product': 'removeProduct',
    'addProduct': 'addProduct',
  },

  render: function() {

    this.$el.children().remove();

    if (this.collection.length) {

      this.$el.html(this.template({}));

      this.collection.each(function(model) {
        this.$el.find('tbody').append(new myApplication.productView({
          model:...