my-articles

Peculiar es6 mapping approach

#javascript #es6 #vuejs 2017-05-21

vue navigation

Today my talk covers response mapping. By mapping I mean converting standard js objects into classed es6 objects. The latter solution is much needed when working with deep nested structures as it allows to bind behaviour to data.

Premise

Let’s say you’re working on order editing form. Order contains product and rent date range.

Imagine you’ve sent a request like this:

/api/orders/show
{
  "id": 10
}

And sure enough you got a response:

{
  "id": 10,
  "name": "Room order",
  "product": {
    "id": 54,
    "name": "Room in fancy hotel",
    "hourly_price": "5.00"
  },
  "date_range": {
    "from": "2017-06-06",
    "to": "2017-06-08"
  }
}

You toss data in component and everything goes fine.

But at some point you decide to show order price in component. Which must be calculated depending on product.hourly_price and order.date_range.

You might implement the calculation logic in component. But that won’t scale. Because exactly the same calculation will be used in CreateOrder and DisplayOrder components. And maybe even OrderList. You never know.

Mapping

Let’s solve the problem OOP way.

Obviously we’d need 3 classes: Order, Product and DateRange. Implementing them one by one won’t take long.

vue navigation

class Order {
  id: Number
  name: String
  dateRange: DateRange
  product: Product

  constructor ({ id, name, dateRange, product } = {}) {
    this.id = id
    this.name = name || ''
    this.dateRange = new DateRange(dateRange) || null
    this.product = new Product(product) || null
  }

  /**
   * Calculate order price.
   * @returns {number}
   */
  calulatePrice (): Number {
    return this.dateRange.calculateDuration() * this.product.hourlyPrice
  }
}
class Product {
  id: Number
  name: String
  hourlyPrice: Number

  constructor ({ id, name, hourlyPrice } = {}) {
    this.id = id
    this.name = name
    this.hourlyPrice = hourlyPrice
  }
}
class DateRange {
  start: Date
  end: Date

  constructor ({ start, end } = {}) {
    this.start = Date(start)
    this.end = Date(end)
  }

  /**
   * DateRange duration in days.
   * @returns {number}
   */
  calculateDuration (): Number {
    // Copy date parts of the timestamps, discarding the time parts.
    const one = new Date(this.start.getYear(), this.start.getMonth(), this.start.getDate())
    const two = new Date(this.end.getYear(), this.end.getMonth(), this.end.getDate())

    // Do the math.
    const millisecondsPerDay = 1000 * 60 * 60 * 24
    const millisBetween = two.getTime() - one.getTime()
    const days = millisBetween / millisecondsPerDay

    // Round down.
    return Math.floor(days)
  }
}

After doing these classes actual mapping is a no-brainer:

new Order(response)

As a result of mapping we get the same tree of data, but now it’s (somewhat) typed and contains behaviour. Also note that children mapping happens free of charge. Meaning that after implementing a bunch of classes different tree configurations will be mapped automagically.

In-component usage

Example component will be EditOrder.vue.

On create we will:

    data () {
      return { editedOrder: null }
    },
    created (){
      // Here we're loading our order by ajax.
      const orderData = getOrderResponse()
      this.editedOrder = new Order(orderData)
    },

And here’s the template itself.

<div class="form" v-if="editedOrder">
    <input type="text" v-model="editedOrder.name">
    <div></div>
    <button @click="save"></button>
</div>

Price will be calculated, just as promised. While component and Order object are clean and testable.

That’s it. As always, I prepared full source code.


Feel free to leave stars on github if you find my work useful : 3. Subscribing is also possible.