LaravelでUIkitのUploadを使う

#Laravelのファイルアップロード
Laravelのファイルアップロードでは、CSRF保護のためにCSRF「トークン」をフォームに含めて送信を行う必要があります。CSS,JSフレームワークのUIkitには[アップロードコンポーネント](https://getuikit.com/docs/upload)が用意されていますが、そのまま使うとCSRF「トークン」が用意されていないため419エラーが出てしまします。

#環境

* Laravel 6
* UIkit 3

#bladeテンプレート
今回は[Drop area](https://getuikit.com/docs/upload#drop-area)を使います。
![drop_area.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155265/269a994b-0273-6a5c-bcf5-1f23ff05be5c.png)

```php:**.blade.php
<div class="js-upload uk-placeholder uk-text-center">
    <span uk-icon="icon: cloud-upload"></span>
    <span class="uk-text-middle">Attach binaries by dropping them here or</span>
    <div uk-form-custom>
        @csrf //CSRFトークン生成のBladeディレクティブを追加
        <input type="file" multiple>
        <span class="uk-link">selecting one</span>
    </div>
</div>

<progress id="js-progressbar" class="uk-progress" value="0" max="100" hidden></progress>

</div>
```

#Script
JavaScriptはblade内に`<script></script>`でおいても別ファイルにして読み込んでもいいです。解説はソースの後！

```javascript:script(js)
var bar = document.getElementById('js-progressbar');

UIkit.upload('.js-upload', {

    csrf_token: $('meta[name="csrf-token"]').attr('content'), //csrfトークンを取得
    name: 'thumbnail', //name指定はここでやります。HTMLのinputには書きません。
    url: '', //送信先のurl、Controllerにファイルを渡します。
    multiple: true,

    beforeSend: function () {
        console.log('beforeSend', arguments);
    },
    beforeAll: function () {
        console.log('beforeAll', arguments);
    },
    load: function () {
        console.log('load', arguments);
    },
    error: function () {
        console.log('error', arguments);
    },
    complete: function () {
        console.log('complete', arguments);
    },

    loadStart: function (e) {
        console.log('loadStart', arguments);

        bar.removeAttribute('hidden');
        bar.max = e.total;
        bar.value = e.loaded;
    },

    progress: function (e) {
        console.log('progress', arguments);

        bar.max = e.total;
        bar.value = e.loaded;
    },

    loadEnd: function (e) {
        console.log('loadEnd', arguments);

        bar.max = e.total;
        bar.value = e.loaded;
    },

    completeAll: function () {
        console.log('completeAll', arguments);

        setTimeout(function () {
            bar.setAttribute('hidden', 'hidden');
        }, 1000);

        alert('アップロードが完了しました');
    }

});
```
UIkitでは各コンポーネントをカスタムできるようにJavaScript compornentが用意されています。

```javascript:UIkit_component
UIkit.upload(element, options);
```
Uploadはこれの機能がやたらと豊富なんですね。オプションで値を送ってやることで色々とカスタマイズできます。ただ、csrfトークンは送れないんですね...
だから渋々ですがcsrfトークンを送って、受け取れるように`uikit.js`を書き換えちゃいます。とりあえず、`options`として`scrf_token`を勝手に作ってcsrfトークンを取得してセットしましょう。

#uikit.js
やたら長いですが、書き足すところはちょっとなのでご容赦を...

```javascript:uikit.js
//11998行目くらいです。
var upload = {

        props: {
            allow: String,
            clsDragover: String,
            concurrent: Number,
            maxSize: Number,
            method: String,
            mime: String,
            msgInvalidMime: String,
            msgInvalidName: String,
            msgInvalidSize: String,
            multiple: Boolean,
            name: String,
            params: Object,
            type: String,
            url: String
        },

        data: {
            allow: false,
            clsDragover: 'uk-dragover',
            concurrent: 1,
            maxSize: 0,
            method: 'POST',
            mime: false,
            msgInvalidMime: 'Invalid File Type: %s',
            msgInvalidName: 'Invalid File Name: %s',
            msgInvalidSize: 'Invalid File Size: %s Kilobytes Max',
            multiple: false,
            name: 'files[]',
            params: {},
            type: '',
            url: '',
            abort: noop,
            beforeAll: noop,
            beforeSend: noop,
            complete: noop,
            completeAll: noop,
            error: noop,
            fail: noop,
            load: noop,
            loadEnd: noop,
            loadStart: noop,
            progress: noop,
            csrf_token: ''//送ったcsrf_tokenを受け取れるようにする。
        },

        events: {

            change: function(e) {

                if (!matches(e.target, 'input[type="file"]')) {
                    return;
                }

                e.preventDefault();

                if (e.target.files) {
                    this.upload(e.target.files);
                }

                e.target.value = '';
            },

            drop: function(e) {
                stop(e);

                var transfer = e.dataTransfer;

                if (!transfer || !transfer.files) {
                    return;
                }

                removeClass(this.$el, this.clsDragover);

                this.upload(transfer.files);
            },

            dragenter: function(e) {
                stop(e);
            },

            dragover: function(e) {
                stop(e);
                addClass(this.$el, this.clsDragover);
            },

            dragleave: function(e) {
                stop(e);
                removeClass(this.$el, this.clsDragover);
            }

        },

        methods: {

            upload: function(files) {
                var this$1 = this;


                if (!files.length) {
                    return;
                }

                trigger(this.$el, 'upload', [files]);

                for (var i = 0; i < files.length; i++) {

                    if (this.maxSize && this.maxSize * 1000 < files[i].size) {
                        this.fail(this.msgInvalidSize.replace('%s', this.maxSize));
                        return;
                    }

                    if (this.allow && !match$1(this.allow, files[i].name)) {
                        this.fail(this.msgInvalidName.replace('%s', this.allow));
                        return;
                    }

                    if (this.mime && !match$1(this.mime, files[i].type)) {
                        this.fail(this.msgInvalidMime.replace('%s', this.mime));
                        return;
                    }

                }

                if (!this.multiple) {
                    files = [files[0]];
                }

                this.beforeAll(this, files);

                var chunks = chunk(files, this.concurrent);
                var upload = function (files) {

                    var data = new FormData();

                    files.forEach(function (file) { return data.append(this$1.name, file); });

                    for (var key in this$1.params) {
                        data.append(key, this$1.params[key]);
                    }

                    ajax(this$1.url, {
                        //ここから
                        headers: {
                            'X-CSRF-TOKEN': this$1.csrf_token,
                        }, //ここまで追加
                        data: data,
                        method: this$1.method,
                        responseType: this$1.type,
                        beforeSend: function (env) {

                            var xhr = env.xhr;
                            xhr.upload && on(xhr.upload, 'progress', this$1.progress);
                            ['loadStart', 'load', 'loadEnd', 'abort'].forEach(function (type) { return on(xhr, type.toLowerCase(), this$1[type]); }
                            );

                            this$1.beforeSend(env);

                        }
                    }).then(
                        function (xhr) {

                            this$1.complete(xhr);

                            if (chunks.length) {
                                upload(chunks.shift());
                            } else {
                                this$1.completeAll(xhr);
                            }

                        },
                        function (e) { return this$1.error(e); }
                    );

                };

                upload(chunks.shift());

            }

        }

    };

```
##ポイント1
まずは、optionsとして送ったcsrf_tokenの受け皿を作るところ。

```javascript
data: {
            allow: false,
            // ~~
            progress: noop,
            csrf_token: ''//送ったcsrf_tokenを受け取れるようにする。
        },
```
##ポイント2
ajaxを使っているので、ajaxに対応したcsrf保護を行います。headersを追加して、`X-CSRF-TOKEN`で値を渡しましょう。[公式ドキュメント](https://laravel.com/docs/6.x/csrf#csrf-x-csrf-token)

```javascript
ajax(this$1.url, {
    //ここから
    headers: {
        'X-CSRF-TOKEN': this$1.csrf_token,
    }, //ここまで追加
    data: data,
    method: this$1.method,
    responseType: this$1.type,
    beforeSend: function (env) {
        // ~~
    }
}).then(
    // ~~
);
```
これで、Controllerに値を渡せるようになります。

#おしまい
Laravelを使って趣味でWebアプリを作って１ヶ月半が経ちました。CSSとJSは自分であまり書きたくないのでフレームワーク何使おうかと色々考えて、なんとなくBootstrapはやだなあと思っていたところに[UIkit](https://getuikit.com/)というフレームワークが刺さりました。シンプルですがめちゃくちゃパワフルで楽しく使っています。ただ情報が少ないんですね... 日本語でのUIkit紹介記事はそこそこあるんですが、実際使って困ったことを共有している人はほとんどいない。そもそもユーザーが少ない可能性は大いにありますが、とりあえず需要あるか実験的に記事投稿していこうかと思います。