Building Rails Params with Vue.js Computed Properties

Jeff
Vue js

Rails is kinda like that friend you have who’s really into foreign films. It knows what it likes and is very picky about what it will accept. Especially with params. If you build your param hashes out correctly in rails, life is swell. Submitting forms is a breeze. You can instantiate a new object, feed it params, and let rails sort out the rest. But if you don’t build out the data for params correctly, submitting forms and updating objects in you db is a royal pain.

Building the correct data structure for params is easy enough with erb or some other rails templating language that builds out html. But what if you are using one of these modern front end frameworks to build you app is javascript? Gasp! Don’t tell DHH!

Let’s look at a pattern that we have come up with here at Littlelines to build out params hashes that play nice with rails using the Vue framework’s computed property functionality.

First a little bit about computed properties:

Computed Properties are basically a way to compose reactive data and update all the linked data in your UI automatically. That was a lot of buzz words at once, so let’s dive in a bit and unpack that.

We have found computed properties to be very useful here when combining other reactive Vue properties (such as ‘data’ or ‘props’) to make other reactive properties. The major benefit of this approach is when one of these combined pieces of data is changed, your computed property is updated automatically to reflect this.

An example:

Say you are selling tickets to a concert. You have an API endpoint that creates a purchase for a user and a performance. You might have a few pieces of data that you want to piece together to build out your UI.

You might have a controller that looks like this:

class PurchasesController < ApplicationController
  def create
    @purchase = Purchase.create(purchase_params)

    if @purchase
      // success
    else
      // render error
    end
  end

  private

  def purchase_params
    params.require(:purchase).permit(
      :performance_id, :user_id, :number_of_tickets,
      :tax, :subtotal, :purchase_total
    )
  end
end

The purchase record you’re creating requires a lot of data (even more than this naive example in a production setting – you’d probably have to deal with things like credit cards, paypal accounts, stripe accounts, etc). This could be a real pain to set with with a traditional form, setting names, hidden inputs for sensitive stuff, calculating some data with javascript and trying to find a way to send that over as well; the list goes on. But this is the future! We have creepy autonomous smart speaker voice assistants and web apps build with javascript!

Let’s check in with the future in this snippet:

<template>
  <form @submit="purchaseTickets()">
    <label for="number_of_tickets">Number of Tickets:</label>
    <select id="number_of_tickets" v-model="numberOfTickets">
      <option v-for="ticketNumber in performance.ticket_total" :key="ticketNumber" :value="ticketNumber">
        {{ ticketNumber }}
      </option>
    </select>
    <label for="zip_code">Zip Code</label>
    <input id="zip_code" type="text" v-model="zipCode">
  </form>
</template>

<script>
export default {
  name: 'buy-tickets',
  props: { performance: Object },

  data () {
    return {
      zipCode: '',
      numberOfTickets: 1
    }
  },

  computed: {
    subtotal () {
      return this.ticketsToPurchase * this.performance.price
    },

    tax () {
      if (this.zipCode) return this.calculateTax(this.zipcode, this.subtotal)
    },

    total () {
      return this.subtotal + this.tax
    },

    params () {
      return {
        purchase: {
          performance_id: this.performance.id,
          zip_code: this.zipCode,
          number_of_tickets: this.numberOfTickets,
          subtotal: this.subtotal,
          tax: this.tax,
          total: this.total
        }
      }
    },
  },

  methods: {
    purchaseTickets () {
      PurchaseService.buyTickets(this.params).then(response => {
        // Success! You did the thing
      }, error => {
        // handle error
      })
    },

    calculateTax (zipCode, subtotal) {
      TaxApi.getTax(zipCode, subtotal).then(response => {
        this.tax = response.body
      }, error => {
        // handle error
      })
    }
  }
}
</script>

Ok, there’s a lot going on here; let’s start with the function that runs when the form is submitted. Form submission will trigger the purchaseTickets function. This is a simple promise that will post to your API and create the purchase. This function is referencing something called this.params. (Don’t be confused by the refernces to ‘this’ scattered throughout this file. ‘this’ references the vue component instance in this case, and it will contain references to all data attributes, props, computed properties, methods, etc of the component, and the current state of these for this specific instance.)

The ‘params’ computed property is the secret sauce of this pattern. It composes references to a prop (this.performance), a data property (this.numberOfTickets), a v-model on a input (this.zipCode), and three other computed properties (tax, subtotal, and total). We can even convert the camelCased js properties to snake_cased keys in the object for ruby, so each languages’ standards are appeased.

I hope you have found this powerful little pattern interesting and see a use for it in your next project! Thanks for reading.