reCAPTCHAが認証が失敗したときに人かどうか確認する画像も表示しないことがあり、原因がよくわかっていなかったので調べてみた。
すると、JavaScriptのコンソールにエラーメッセージが出ていた。
Uncaught (in promise) timeout
とりあえずこれでググると、以下のissueがヒットした。
コメントの中に、react-google-recaptchaでも起きているという話があったので、ここでようやく「ははぁ、これはコンポーネントのレンダリングが終わる前にreCAPTCHAが何かしようとしておかしくなるのだな」とあたりが付いた。
そして、reCAPTCHAのページを見ると、自分で制御して明示的にレンダリングすることもできることがわかった。
ということで、それらを念頭に置いて直していくことにした。
knockout.jsでreCAPTCHAと連携するViewModelを定義する
カスタムバインディングを定義する
どうするのがいいのかな〜と悩みながら適当にググったら、参考になりそうなgistを見つけた。
これを参考に、knockout.jsのカスタムバインディングを作った。
そのgistのURLはこちら。
https://gist.github.com/patorash/ccfef86bb04c03f81ead161659554ccc
ko.bindingHandlers.recaptcha = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var propWriters = allBindings()['_ko_property_writers'];
var value = valueAccessor();
document.addEventListener('createCaptcha', function(event, theme) {
var site_key = document.querySelector('.g-recaptcha').dataset.sitekey
var callback = allBindings.get('recaptchaCallback') || function () {
if (!value) {
if (ko.isObservable(value)) {
value = true
} else {
propWriters.recaptcha(true)
}
}
};
window.widgetId = grecaptcha.render('recaptcha', {
sitekey: site_key,
theme: theme,
callback: callback,
'expired-callback': function() {
grecaptcha.reset(widgetId);
if (ko.isObservable(value)) {
value = false
} else {
propWriters.recaptcha(false)
}
}
})
});
}
};
ko.expressionRewriting._twoWayBindings['recaptcha'] = true;
これを使う。
Knockout componentの修正
まだCoffeeScriptを使っているのでCoffeeScript表記である。
2019-02-15 追記
componentLoaded
メソッドでcreateCaptcha
イベントを発火していますが、変数grecaptcha
が存在する前(つまりreCAPTCHAのコードがロードし終わる前)に発火してエラーになるケースがあったので、setInterval
でgrecaptcha
ができるまで待つようにしました。
# ko.bindingHandlers.recaptchaを使っています。
class InquiryForm
constructor: ->
@name = ''
@email = ''
@subject = ''
@message = ''
@recaptcha_verified = false
ko.track(this)
componentLoaded: ->
timer_id = setInterval( ->
if grecaptcha?
clearInterval(timer_id)
document.dispatchEvent(new Event('createCaptcha'))
, 100)
enableSubmit: ->
!_.isBlank(@name) and
!_.isBlank(@email) and
!_.isBlank(@subject) and
!_.isBlank(@message) and
@recaptcha_verified
ko.components.register 'inquiry-form', {
viewModel: -> new InquiryForm()
template: { element: 'ko-normal-template' }
}
ちなみにko-normal-templateはコンポーネントタグの中をそのまま出すだけのやつ。
template id="ko-normal-template"
/! ko template: { nodes: $componentTemplateNodes }
/! /ko
HTML側の修正
自分がやってるのがRailsプロジェクトなのでslim表記、かつ、gem recaptchaを使っているので、そのヘルパーメソッドを使う。
reCAPTCHAのRECAPTCHA_SITE_KEY、RECAPTCHA_SECRET_KEYは.envに定義している。
inquiry-form
form data-bind="recaptcha: recaptcha_verified, template: { afterRender: componentLoaded }"
/ 他のバインディングしている要素は省略
= recaptcha_tags render: 'explicit'
#recaptcha
button type="submit" data-bind="enable: enableSubmit()"
肝はafterRender: componentLoaded
で、コンポーネントのレンダリングが終わったのを確認後にcreateCaptcha
イベントを発火している。
createCaptcha
イベントはカスタムバインディングの方でイベントリスナーを追加しているから、そこでreCAPTCHAのrenderが呼ばれて、レンダリングが行われる。
reCAPTCHAにチェックが入って認証が成功すれば、InquiryForm
の@recaptcha_verified
がtrueになるという仕組み。
今後の課題
afterRenderを使ってイベント発火を行わなければならないのが微妙にイケてないのだけれど、他にいい方法が思いつかなかった。なにかよい方法があれば教えていただきたい。