0

I am trying to build a form that allows a user to make a booking for a martial arts class they wish to attend. I have created a form that dynamically changes based on the selections the user makes, when I change any of the select options the form updates and when I submit the form it redirects to a Stripe checkout. The problem I have is after submitting the the form and I click the browsers back button or the back button provided on the Stripe checkout page the select options that have been updated have reverted back to the default options rather than the updated options. Can anyone help me correct this behaviour and get the correct form elements to persist? Here is the code I am using to do this: The view I am rendering the form in:

<% content_for :banner_title, @page_data['bannerTitle'] %>
<% content_for :head do %>
    <meta name="turbolinks-cache-control" content="no-cache">
<% end %>

<div class="content container py-5">
    <div class="row">
        <div class="col-12 col-md-7 mx-auto">
            <%= render "forms/booking", options: @options %>
        </div>
    </div>
</div>

The form I am using:

<%= form_for @booking, class: 'booking clearfix' do |f| %>
    <div class="form-group">
        <%= f.label(:class_name, "Select a class you wish to attend: ") %>
        <%= f.select(:class_name, options_for_select(options[:class_names], @booking.class_name), {}, class: 'form-control' ) %>
    </div>

    <div class="form-group">
        <%= f.label(:date, "Select a date:") %>
        <%= f.select(:date, options_for_select( options[:dates], @booking.date ), {}, class: 'form-control' ) %>
    </div>

    <div class="form-group">
        <%= f.label(:time, "Select a time: ")%>
        <%= f.select(:time, options_for_select(options[:times], @booking.time), {}, class: 'form-control') %>
    </div>

    <div class="form-group">
        <%= f.label(:attendees, "How many attending: ") %>
        <%= f.select(:attendees, options_for_select(options[:attendees], @booking.attendees), {}, class: 'form-control' )%>
    </div>
    <%= f.submit 'Place Booking', class: 'btn btn-primary btn-lg text-light float-right', id: 'create-booking' %>
<% end %>

<%= javascript_pack_tag 'booking_form' %>
<script src="https://js.stripe.com/v3/"></script>

The model for the form (I'm not using ActiveRecord, i dont know if this makes any difference?):

class Booking
    include ActiveModel::Model

    MAX_ATTENDEES = 10
    attr_accessor :time, :class_data, :attendees, :date, :class_name

    def initialize(args={})
        @time = args['time']
        @class_name = args['class_name']
        @class_data = args['class_data']
        @date = args['date']
        @attendees = args['attendees']
    end

    def day
        @date.split(',').first
    end

    def available_dates
        days_index_array = class_data['times'].keys.map {|k| day_index(k) }
        days_within( days_index_array ) 
    end

    def available_times
        if !date
            class_data['times'][class_data['times'].keys.first.downcase]
        else
            class_data['times'][day.downcase]
        end
    end

    def total_cost
        @class_data['cost'].to_i * @attendees.to_i
    end

    def attending_string
        ActionController::Base.helpers.pluralize(attendees, 'person')
    end

    private

    def days_within(days, timeframe=1.month)
        start_date = Date.tomorrow
        end_date = start_date + timeframe
        (start_date..end_date).to_a.select {|k| days.include?(k.wday) }
    end

    def day_index(day)
        DateTime::DAYNAMES.index(day.to_s.capitalize)
    end
end

And the controller I am calling the new action in:

class BookingsController < ApplicationController
    include BookingsHelper
    before_action :set_class_data
    skip_before_action :set_page_data, except: :new

    def new
        set_booking
        
        # store values to be passed to the form helper method options_for_select. Each value must be an array populated with arrays with the format [value, text]
        @options = {
            class_names: @class_data.map {|c| [ c['name'], c['name'] ]}, 
            dates: @booking.available_dates.map {|d| [d.strftime('%A, %d %B'), d.strftime('%A, %d %B')] },
            times: @booking.available_times.map {|t| [t,t]},
            attendees: Booking::MAX_ATTENDEES.times.map {|i| [i+1, i+1]}
        }
    end

    def create
    end

    def booking_form_data
        booking_form_data = set_booking_form_data(params)    
        update_session_booking(booking_form_data)
        render json: booking_form_data
    end
    
    private
    
    def set_booking
        if session[:current_booking]
            pp "session exists"
            @booking = Booking.new(session[:current_booking])
        else
            pp "session does not exist"
            @booking = Booking.new
            session[:current_booking] = @booking.instance_values
        end
        set_booking_class_data
    end

    def set_booking_class_data
        !@booking.class_name ? @booking.class_data = @class_data.first.except('information') : @booking.class_data = @class_data.find {|cd| cd['name'] == @booking.class_name}.except('information')
    end
    
    def booking_params
        params.permit(:class_name, :date, :time, :attendees, :update_type)
    end

    def update_session_booking(booking_form_data)
        if params[:update_type] == 'class_name'
            session[:current_booking]['class_name'] = params[:class_name]
            session[:current_booking]['date'] = booking_form_data[:date_options].first
            session[:current_booking]['time'] = booking_form_data[:time_options].first
        elsif params[:update_type] == 'date'
            session[:current_booking]['date'] = params[:date]
            session[:current_booking]['time'] = booking_form_data[:time_options].first
        elsif params[:update_type] == 'time'
            session[:current_booking]['time'] = params['time']
        elsif params[:update_type] == 'attendees'
            session[:current_booking]['attendees'] = params[:attendees]
        elsif params[:update_type] == 'load'
            session[:current_booking] = booking_params.except(:update_type) 
        end
        pp "Session Booking: #{session[:current_booking]}"
    end

    def set_booking_form_data(params)
        booking_form_data = {}
        selected_class = @class_data.find {|cd| cd['name'] == params[:class_name] }
        # when the class_name select is changed
        if params[:update_type] == 'class_name'
            booking_form_data[:date_options] = days_within( selected_class['times'].keys.map {|k| day_index(k) } ).map {|d| d.strftime('%A, %d %B') }
            booking_form_data[:time_options] = selected_class['times'][booking_form_data[:date_options].first.split(',')[0].downcase]
        # when date select is changed
        elsif params[:update_type] == 'date'
            booking_form_data[:time_options] = selected_class['times'][params[:date].split(',')[0].downcase]
        end
        booking_form_data
    end
end

And the javascript I am using to update the form:

getBookingFormData = (bodyData={}, successCallback=()=>{}) => {
    $.ajax({
        url: '/booking_form_data',
        method: 'POST',
        beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
        data: bodyData,
        success: successCallback
    })
}

createOptions = (values) => {
    let newOptions = [];
    $.each(values, (index, value) => {
        let newOption = $('<option></option>');
        newOption.attr('value', value);
        newOption.text(value);
        newOptions.push(newOption)
    })
    return newOptions
}

appendOptions = (options, element) => {
    $(element).empty();
    $(element).append(options)
}

currentFormValues = () => {
    return {
        class_name: $('#booking_class_name').val(),
        date: $('#booking_date').val(),
        time: $('#booking_time').val(),
        attendees: $('#booking_attendees').val()
    }
}

$('select#booking_class_name').on('change', () => {
    let bodyData = { 
        class_name: $('select#booking_class_name').val(),
        update_type: 'class_name'
    }
    let successCallback = (res) => {
        let dateOptions = createOptions(res.date_options);
        let dateSelect = $('select#booking_date');
        let timeOptions = createOptions(res.time_options);
        let timeSelect = $('select#booking_time');
        appendOptions(dateOptions, dateSelect);
        appendOptions(timeOptions, timeSelect);
    }
    getBookingFormData(bodyData, successCallback)
});

$('select#booking_date').on('change', () => {
    let bodyData = {
        class_name: $('select#booking_class_name').val(),
        date: $('select#booking_date').val(),
        update_type: 'date'
    };
    let successCallback = (res) => {
        let timeOptions = createOptions(res.time_options);
        let timeSelect = $('select#booking_time');
        appendOptions(timeOptions, timeSelect);
    }
    getBookingFormData(bodyData, successCallback)
});

$('select#booking_time').on('change', () => {
    let bodyData = {
        time: $('select#booking_time').val(),
        update_type: 'time'
    };
    getBookingFormData(bodyData);
});

$('select#booking_attendees').on('change', () => {
    let bodyData = {
        attendees: $('select#booking_attendees').val(),
        update_type: 'attendees'
    };
    getBookingFormData(bodyData);
});


$('#create-booking').on('click',(e) => {
    e.preventDefault();
    bookingDefault = false
    const stripe = Stripe(process.env.STRIPE_PUBLIC);
    
    let requestHeaders = new Headers({
        'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
        'Content-Type': 'application/json'
    })

    fetch('/create_checkout_session', { 
        method: 'POST',
        headers: requestHeaders,
        body: JSON.stringify(currentFormValues()) 
    })
    .then((res) => { return res.json() })
    .then((session) => { return stripe.redirectToCheckout({ sessionId: session.id }) })
    .then((result) => { 
        if (result.error) { alert(result.error.message) }
    })
    .catch((error) => { console.error('Error: ', error) })
})

From what Ive read i think it may be a problem related to caching which makes me think this is an issue with turbolinks but I could be completely wrong. Ive tried adding meta tags that disable turbolinks or force it to reload the page but they did not seem to work. Any input at all would be really appreciated as Ive been stuck on this for days. Let me know if you need any more information

Nolan H
  • 6,205
  • 1
  • 5
  • 19
antonyCas
  • 81
  • 2
  • 8

1 Answers1

0

This isn't so much Stripe-related as it is related you your form value management. If you want to keep these values around, you'll need to build that into your front application, somehow. There are lots of options for this:

  • Using local storage
  • Using query parameters, if not sensitive info
  • Using a cookie and a server session you can re-retrieve and hydrate the f.select options with a default value.
Nolan H
  • 6,205
  • 1
  • 5
  • 19