【Rails】FormのDirtyCheckをする（with Turbolinks, CKEditor）

フォームなどで保存されていない内容を含むときに，ページを離れようとしている場合に次のようなアラートを出す．
Turbolinksを有効にしていて，`form_for` でつくられた普通のフォームや，WYSIWYGエディタ[ckeditor](https://github.com/galetahub/ckeditor)を使っているケースに対応してみる．
jQuery製の [AreYouSure](https://github.com/codedance/jquery.AreYouSure) や [dirtyForms](https://github.com/snikch/jquery.dirtyforms)　でもいいが，細かい調整したかったので自作した．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/c73255d1-bf63-7cf6-dba0-d0d474eef37e.png)


# 検証環境

- Rails 5.1.4
- Turbolinks 5
- WYSIWYGとして[ckeditor](https://github.com/galetahub/ckeditor)を使用している


# 普通フォームのDirtyCheck

`beforeunload` イベントで `window.appName.formDirty` だけチェックしていると，フォームがサブミットされたタイミングでもDirtyとみなしてしまうので， `window.appName.formSubmit` でサブミットかどうかをチェックしている．

```javascript:app/assets/dirty-forms.js
$(window)
    // フォームの状態を初期化
    .on('turbolinks:load', function () {
        if (!window.appName) window.appName = {};
        window.appName.formDirty = false;
        window.appName.formSubmit = false;
        window.appName.dirtyMessage = '保存していない内容が失われますが、よろしいですか';

        $(':input').change(function () {
            if (!window.appName.formDirty) window.appName.formDirty = true;
        });
        $('form').submit(function () {
            window.appName.formSubmit = true;
        });
    })
    // Turbolinksでページが切り替わるタイミング
    .on("page:before-change turbolinks:before-visit", function () {
        if (window.appName.formDirty) {
            return confirm(window.appName.dirtyMessage);
        }
    })
    // リロードされたり，タブやウインドウが閉じられる前のタイミング
    .bind("beforeunload", function (event) {
        if (!window.appName.formSubmit && window.appName.formDirty) {
            event.returnValue = window.appName.dirtyMessage;
            return event.returnValue;
        }
    });
```

# CKEditorのDirtyチェック

すべてのCKEDITORのDirtyをチェックするカスタムメソッド `hasDirtyForm` を実装する．
CKEDITOR.instancesは [checkDirty](https://docs.ckeditor.com/ckeditor4/docs/#!/api/CKEDITOR.editor-method-checkDirty) メソッドを持つので，そのどれかがDirtyかチェックしているだけ．



```javascript:app/assets/ckeditor/index.js
//= require ckeditor/init.js.erb
//= require ./config
//`app/assets/ckeditor/config.js` では `CKEDITOR.editorConfig` を設定したり 
// `dialogDefinition` イベントを設定しているが内容は割愛．

// .ckeditorが付いている要素をWYSIWYG化する
$(document).on('turbolinks:load', function () {
    $('.ckeditor').each(function () {
        CKEDITOR.replace($(this).attr('id'))
    });
});

// すべてのCKEDITORのDirtyをチェックするカスタムメソッドを実装
CKEDITOR.hasDirtyForm = function () {
    var editors = this.instances;
    return Object.keys(editors).some(function (key) {
        return editors[key].checkDirty();
    });
};
```

`page:before-change turbolinks:before-visit` と `beforeunload` イベント時に `CKEDITOR.hasDirtyForm()` のチェックをするようにする．

```javascript:app/assets/dirty-forms.js
$(window)
    •••
    .on("page:before-change turbolinks:before-visit", function () {
        if (window.appName.formDirty || CKEDITOR.hasDirtyForm()) {
            return confirm(window.appName.dirtyMessage);
        }
    })
    .bind("beforeunload", function (event) {
        if (!window.appName.formSubmit && (window.appName.formDirty || CKEDITOR.hasDirtyForm()) {
            event.returnValue = window.appName.dirtyMessage;
            return event.returnValue;
        }
    });
```