Skip to content

Add form with Turbolinks, Recaptcha, and errors handling

Kishan Jadav edited this page Dec 19, 2022 · 5 revisions

Demo of an almost full cycle of the use of recaptcha v3 and v2 with Turbolinks (and error handling bonus)

# controller > Users

def create
  @user = User.new(user_params)
  recaptcha_v3_success = verify_recaptcha(action: '#', minimum_score: 0.5)
  recaptcha_v2_success = verify_recaptcha(secret_key: ENV['RECAPTCHA_SECRET_KEY_V3']) unless recaptcha_v3_success

  if (recaptcha_v3_success || recaptcha_v2_success) and @user.save
    ...
    head :ok # redirect with turbolinks
  else
    unless recaptcha_v3_success
      @show_checkbox_recaptcha = true
      # if there is no users erros it's recaptcha error # so we add this as error
      @user.errors[:base] << "Please fill the recaptcha" unless @user.errors.any?
    end
    #render :new, :layout => 'no_head_foot'
    render :new, status: 422#, layout: false # status is needed to update page on with error
  end
end

Load with Sprockets:

# js -> users_new.js.coffee
class @users_new
  constructor: ->
    on_error_replace_page_with_recived_respons = (data)->
      $(document.body).html data.responseText.match(/<body[^>]*>((.|[\n\r])*)<\/body>/im)[0]
      # load will load the scripts again
      Turbolinks.dispatch 'turbolinks:load' # will reload js if this is how your turbolinks works
      scrollTo(0, 0)
      # This is application-dependent, but a nice touch.
      # $('.field_with_errors input:first').focus()

    update_recaptcha_token = (el_id,token)->
      hiddenInput = document.getElementById(el_id)
      hiddenInput.value = token
      hiddenInput.innerHTML = '' # for some reason, Recaptcha populates the input with DOM elements that add additional token param on submit (with nil value), I think it cause rails not notice the correct param.

    form = document.getElementByid('your_form_id')
    $(form).on 'ajax:success', (e, data, status, xhr) ->
      Turbolinks.visit('some_page')
    $(form).on 'ajax:error', (e, data, status, xhr) ->
      on_error_replace_page_with_recived_respons data

    $(form).on 'click', ':submit' ()->
      $(form).submit()

Not Recaptcha directly, but will help a lot with Turbolinks, as it will load specific js to every page if exist, in our case on error with the form, the browser garbage collector will remove our previous js listeners with the replaced HTML elements, and call the new js for the page with the error. fixes the duplicate token problem, with loading new token on reload.

# js > init.js.coffee
class Init
  constructor: ->
    ... # some code every page uses
    # find if page have own js
    page = gon.controller_full # 'users_new' # you don't have to use gon, you can read controller name from body 'document.body.className'
    @execute_page_js(page)

  execute_page_js: (page) ->
    # check if method exist if true execute
    if 'function' is typeof window[page]
      klass = window[page]
      new klass()

# will load the right js for every page by controller-action name
document.addEventListener "turbolinks:load", ->
  new Init()

load with Webpacker:

The way to load page-specific js code is changed, unlike sprockets, with webpack all the javascript is conserved inside modules, so we need to declare it before we can call it (and also otherwise they are not added to our file)

// javascript/packs/application.js
import { posts_show } from '../per_page/posts_show';
import { users_new } from '../per_page/users_new';

const pages = { posts_show, users_new };

document.addEventListener("turbolinks:load", () => {
    // I am using gon to save the page name, but you can add page name to document body and then const page = document.body.className
    const page = gon.controller_full
    // check if method exist if true execute
    if ('function' === typeof pages[page]){
      new pages[page]
    }
});

// we need to add this method to windows otherwise our callback will not find it
// not realy happy with this solution, but could not come up with anything better
window.update_recaptcha_token = (el_id,token) => {
    let hiddenInput = document.getElementById(el_id)
    hiddenInput.value = token
    hiddenInput.innerHTML = ''
}

And lastly to import and call our page we need to export it as:

// javascript/per_page.users_new.js
import { on_error_replace_page_with_recived_respons } from './methods/replace_page_from_response'

export class users_new {
    constructor() {
        form = document.getElementByid('your_form_id')
        $(form).on('ajax:success', (event) => {
            Turbolinks.visit('some_page')
        });
        $(form).on('ajax:error', (event) => {
            data = event.detail[0]
            on_error_replace_page_with_recived_respons(data)
        });
    }
}

You may notice that we importing on_error_replace_page_with_recived_respons as we moved it to separate a separate file, wrapping with 'module', this way we can call it inside other page-specific files, and Webpack will do all the job of organizing the methods for us.

// javascript/per_page/methods/on_error_replace_page_with_recived_respons.js
export const on_error_replace_page_with_recived_respons = (data) -> {
  $(body).html(data.body.innerHTML);
}

// load will load the scripts again
Turbolinks.dispatch 'turbolinks:load' # will reload js if this is how your turbolinks works
scrollTo(0, 0)

Our view:

# view > new.html.erb > users_new
<% if @show_checkbox_recaptcha %>
  <%= recaptcha_tags site_key: ENV['RECAPTCHA_SITE_KEY_V2'] %> # we need to add SITE_KEY for v2, as the v3 uses 'Recaptcha.configure'
<% else %>
  <%= recaptcha_v3(action: '#', turbolinks: true, callback: 'update_recaptcha_token') %>
<% end %>