fabOnReact
fabOnReact

Reputation: 5942

javascript objects - retrieve child class variable

The question has a javascript and coffescript jsfiddle at the bottom of the question.

Both fiddle include explanatory comments that needs to be read in a specific order, it print out values to the console when you click on product or submit div, in addition I give you this basic explanation of my issue.


My coffeescript jsfiddle is at the following link

Click below to open the javascript fiddle.

(function() {
  var Item, Product, Purchase,
    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  Purchase = (function() {
    function Purchase() {
      /* on $(document).ready a new Purchase is created */ 
      this.submit = $('#submit');
      /* for each div.product a new Product instance is created */
      this.products = $.map($('.product'), function(product, i) {
        return new Product(product);
      });
      / @onSubmit() */
      
      /* Comment 3) 
      My issue here is how to I access the this.items from the Purchase class and call serialize()?
      onSubmit: function () {
        @submit.click(function(){console.log(Product.serialize())};
      }     */
    }

    return Purchase;

  })();

  Product = (function() {
    Product.items = [];

    function Product(product) {
      this.product = $(product);
      this.id = this.product.data("id");
      this.submit = $('#submit');
      this.setEvent();
      this.onSubmit();
    }

    Product.prototype.setEvent = function() {
      return this.product.click((function(_this) {
        return function() {
          /* Comment 1)
             Product.items is a class variable of Product, because I need to access it from the Purchase class and send post request. When the user clicks on the $('submit') button*/
          Product.items.push(new Item(_this.id));
          return console.log(Product.items);
        };
      })(this));
    };

    Product.prototype.onSubmit = function() {
      return this.submit.click(function() {
      /* Comment 2) 
      This works as you can see, but we have 4 products and this operation will 
      be performed 4 times. I want to achieve this in the Purchase object so it is perfomed only once, by creating a sumit event handler in Purchase */      
        return console.log(Product.serialize());
      });
    };

    Product.serialize = function() {
      var item;
      return {
        items_attributes: (function() {
          var j, len, ref, results;
          ref = Product.items;
          results = [];
          for (j = 0, len = ref.length; j < len; j++) {
            item = ref[j];
            results.push(item.serialize());
          }
          return results;
        })()
      };
    };

    return Product;

  })();

  Item = (function() {
    function Item(product_id) {
      this.product_id = product_id;
      this.serialize = bind(this.serialize, this);
    }

    Item.prototype.serialize = function() {
      return {
        product_id: this.product_id.toString()
      };
    };

    return Item;

  })();

  $(document).ready(function() {
    return new Purchase();
  });

}).call(this);
.console {
  background-color: grey;
  color: white;
  height: 500px;
}      # I print to the console Product.items 

h4 {
  color: red;
  width: 100%;
  text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<ul>
  <li class="product" data-id="1">Product 1</li>
  <li class="product" data-id="2">Product 2</li>
  <li class="product" data-id="3">Product 2</li>
  <li class="product" data-id="4">Product 3</li>
  <li class="product" data-id="5">Product 4</li>
  <div id="submit">Create Purchase</div>
</ul>

<h4>check logs by opening the console</h4>

as I write opensource, you can review my commit history, the specific commit and fork the project

Upvotes: 2

Views: 344

Answers (3)

caffeinated.tech
caffeinated.tech

Reputation: 6548

I've updated your coffeescript fiddle to work as per the questions in the code comments.

Here is my updated version.

I've changed your class structure so that there is no need for any static variables, which seems like a hack to get around a bad design in this case.

You have created your model structure as:

  • one purchase has many products
  • one product has many items

But your post data format requirement shows that:

  • one purchase has many items
  • one item belongs to one product (by reference id)

To get around this inconsistency I flatten the serialized data from products so that items_attributes is an array of serialized item objects:

class Purchase
  ...
  serialize: =>
    items = (product.serialize() for product in @products)
    # flatten array of array of items:
    items_attributes: [].concat.apply([], items)

This cryptic looking line [].concat.apply([], items) is a shorthand for flattening one level deep of nested arrays (taken from this answer).

And each instance of product now saves an array of items on itself rather than statically on the class.

class Product  
  constructor: (product) ->
    @product = $(product)
    @id = @product.data("id")
    @submit = $('#submit')
    @items = []
    @registerEvents()

  addItem: =>
    console.log "added item #{@id}"
    @items.push new Item(@id) 

  registerEvents: ->
    @product.click @addItem

  serialize: =>
    (item.serialize() for item in @items)

I think a better redesign of this class structure would be to remove either the Product or Item class, as there is only a product id, and as far as I can tell, items are like a counter of how many units of a product are being purchased. Instead of having a class for this, you could keep an integer value on the product:

As a fiddle

class Purchase
  constructor: () -> 
    # on $(document).ready a new Purchase is created
    @submit = $('#submit')
    # for each div.product a new Product instance is created
    @products = $.map $('.product'), (product, i) -> 
      new Product(product)
    @registerEvents()

  onSubmit: => 
    console.log "send to server..."
    console.log JSON.stringify(@serialize(), null, 2)

  registerEvents: -> 
    @submit.click @onSubmit

  serialize: =>
    items_attributes: (product.serialize() for product in @products when product.amount isnt 0)

class Product  
  constructor: (product) ->
    @product = $(product)
    @id = @product.data("id")
    @submit = $('#submit')
    @amount = 0
    @registerEvents()

  addItem: =>
    console.log "added item #{@id}"
    @amount++

  registerEvents: ->
    @product.click @addItem

  serialize: =>
    product_id: @id
    amount: @amount

The output now looks different but is IMO cleaner:

new:

{
  "items_attributes": [
    {
      "product_id": 1,
      "amount": 1
    },
    {
      "product_id": 2,
      "amount": 3
    }
  ]
}

old:

{
  "items_attributes": [
    {
      "product_id": "1"
    },
    {
      "product_id": "2"
    },
    {
      "product_id": "2"
    },
    {
      "product_id": "2"
    }
  ]
}

But this may not work well with your current backend implementation, depending on how duplicates are currently handled, so disregard this last part if any legacy constraints cannot be changed.


Lastly, I wanted to add that this "object-oriented" method of attaching event listeners and logic to the DOM is a more structured way than typical jquery functions executed on load. But I've used it in the past, and keeping both DOM structure and code updated is a pain and often leads to bugs due to code changes in one not being mirrored on the other.

As an alternative, I would strongly suggest looking into reactjs or a similar DOM-abstraction type library. These allow you to strongly couple your logic to the view elements which they depend upon.

While usually used with JSX, it couples well with Coffeescript, but there are very few resources on this. Arkency write a good blog about react + coffeescript and I wrote a short post comparing coffeescript to jsx too.

Upvotes: 1

Munim Munna
Munim Munna

Reputation: 17556

You can simply bind the event inside your Purchase object when it is initialized.

this.submit.click(function() {
    return console.log(Product.serialize());
});

Working Snippet: I have commented out onSubmit of Product.

(function() {
  var Item, Product, Purchase,
    bind = function(fn, me) {
      return function() {
        return fn.apply(me, arguments);
      };
    };

  Purchase = (function() {
    function Purchase() {
      /* on $(document).ready a new Purchase is created */
      this.submit = $('#submit');
      /* for each div.product a new Product instance is created */
      this.products = $.map($('.product'), function(product, i) {
        return new Product(product);
      });
      / @onSubmit() */

      /* Comment 3) 
      My issue here is how to I access the this.items from the Purchase class and call serialize()?
      onSubmit: function () {
        @submit.click(function(){console.log(Product.serialize())};
      }     */
      this.submit.click(function() {
        return console.log(Product.serialize());
      });
    }

    return Purchase;

  })();

  Product = (function() {
    Product.items = [];

    function Product(product) {
      this.product = $(product);
      this.id = this.product.data("id");
      this.submit = $('#submit');
      this.setEvent();
      // this.onSubmit();
    }

    Product.prototype.setEvent = function() {
      return this.product.click((function(_this) {
        return function() {
          /* Comment 1)
             Product.items is a class variable of Product, because I need to access it from the Purchase class and send post request. When the user clicks on the $('submit') button*/
          Product.items.push(new Item(_this.id));
          return console.log(Product.items);
        };
      })(this));
    };

    // Product.prototype.onSubmit = function() {
    //   return this.submit.click(function() {
    //     /* Comment 2) 
    //     This works as you can see, but we have 4 products and this operation will 
    //     be performed 4 times. I want to achieve this in the Purchase object so it is perfomed only once, by creating a sumit event handler in Purchase */
    //     return console.log(Product.serialize());
    //   });
    // };

    Product.serialize = function() {
      var item;
      return {
        items_attributes: (function() {
          var j, len, ref, results;
          ref = Product.items;
          results = [];
          for (j = 0, len = ref.length; j < len; j++) {
            item = ref[j];
            results.push(item.serialize());
          }
          return results;
        })()
      };
    };

    return Product;

  })();

  Item = (function() {
    function Item(product_id) {
      this.product_id = product_id;
      this.serialize = bind(this.serialize, this);
    }

    Item.prototype.serialize = function() {
      return {
        product_id: this.product_id.toString()
      };
    };

    return Item;

  })();

  $(document).ready(function() {
    return new Purchase();
  });

}).call(this);
.console {
  background-color: grey;
  color: white;
  height: 500px;
}

h4 {
  color: red;
  width: 100%;
  text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<ul>
  <li class="product" data-id="1">Product 1</li>
  <li class="product" data-id="2">Product 2</li>
  <li class="product" data-id="3">Product 2</li>
  <li class="product" data-id="4">Product 3</li>
  <li class="product" data-id="5">Product 4</li>
  <button type="button" id="submit">Create Purchase</button>
</ul>

<h4>check logs by opening the console</h4>

Upvotes: 2

caffeinated.tech
caffeinated.tech

Reputation: 6548

I'm a fan of the Active Model Serializer gem which is now a part of Rails. I would try expanding this pattern into your coffeescript by adding a serialize method to all of your classes, and call these when you pass data to your server.

I'm not sure on your plans for the Item class, so here is a simple mockup with the proposed serialize method:

class Item
  constructor: (@purchase, @product, @quantity) ->

  serialize: =>
    purchase_id: @purchase.id.toString()
    product_id: @product.id.toString()
    quantity: parseInt(@quantity)

Given that your purchase class will have an array of @items, then the Purchase's serialize method would look something like this:

serialize: =>
  items_attributes: (item.serialize() for item in @items)

And your ajax post would then use the serialize method:

$.ajax
   url: "/items"
   method: "POST"
   dataType: "json"
   data: 
     purchase: @serialize()
   error: (jqXHR, textStatus, errorThrown) ->
   success: (data, textStatus, jqXHR) ->

Then you should get a JSON post body of

'purchase' => {
  'items_attributes' => [
    {
      'purchase_id' => '1'
    },
    {
      'purchase_id' => '2'
    }
  ]
}

which you can use within your rails controller via strong params:

params.require(:purchase).permit(item_attributes: [:purchase_id])

Upvotes: 2

Related Questions