Help us understand the problem. What is going on with this article?

Knockout.jsとreCAPTCHAを連携させる

More than 1 year has passed since last update.

reCAPTCHAが認証が失敗したときに人かどうか確認する画像も表示しないことがあり、原因がよくわかっていなかったので調べてみた。
すると、JavaScriptのコンソールにエラーメッセージが出ていた。

Uncaught (in promise) timeout

とりあえずこれでググると、以下のissueがヒットした。

https://github.com/google/recaptcha/issues/269

コメントの中に、react-google-recaptchaでも起きているという話があったので、ここでようやく「ははぁ、これはコンポーネントのレンダリングが終わる前にreCAPTCHAが何かしようとしておかしくなるのだな」とあたりが付いた。

そして、reCAPTCHAのページを見ると、自分で制御して明示的にレンダリングすることもできることがわかった。

https://developers.google.com/recaptcha/docs/display#explicit_render

ということで、それらを念頭に置いて直していくことにした。

knockout.jsでreCAPTCHAと連携するViewModelを定義する

カスタムバインディングを定義する

どうするのがいいのかな〜と悩みながら適当にググったら、参考になりそうなgistを見つけた。

https://gist.github.com/bh3605/98d495bc05ee5428ed62893c0810449a

これを参考に、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のコードがロードし終わる前)に発火してエラーになるケースがあったので、setIntervalgrecaptchaができるまで待つようにしました。

# 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を使ってイベント発火を行わなければならないのが微妙にイケてないのだけれど、他にいい方法が思いつかなかった。なにかよい方法があれば教えていただきたい。

patorash
Ruby, Railsをメインにやってます。Ruby GoldとOSS-DB Silverを取得してます。岡山の勉強会界隈に時々出没してます。昔はPHPもやってました。
https://patorash.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away