-
Notifications
You must be signed in to change notification settings - Fork 444
Add form with Turbolinks, Recaptcha, and errors handling
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
# 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()
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)
# 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 %>