【LWC】LEXで添付ファイル（Attachment）を利用したい場合に。

## LEXで添付ファイルを使いたい
Classic時代からSalesforceを利用している場合、LEX移行時にファイルの取り扱いが課題になったりしますよね。

>[Salesforce Classic で添付したファイルが Lightning Experience で検索できない](https://help.salesforce.com/articleView?id=000314087&language=ja&type=1&mode=1)
>添付ファイルは Lightning Experience で廃止されたため、これは期待された動作です。
>添付ファイルは、より頑強な「ファイル」機能により置き換えられました。


もちろんLEXを使えればそれでいいですが、Classicで機能を開発している場合は、スムーズにいかない場合もあるでしょう。

出番は少ないと思いますが、LEXで添付ファイル（Attachment）を利用するためのLWCをご紹介します。

## Sample:添付ファイルアップローダ

![lwc_attachment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97406/aeac720c-559f-63ae-cebf-4f9415bfbf04.png)

主な機能は下記のとおりです。
1. 添付ファイル(Attachment)のアップロード
2. 添付ファイル(Attachment)の一覧表示（プレビューあり）
3. 添付ファイル(Attachment)の削除

以下、サンプルコードです。

##html
```html:attachmentFileUploader.html
<template>
    <lightning-card icon-name="standard:file">
        <h1 slot="title"><strong>添付ファイルアップローダ</strong></h1>
        <div>
            <div style="margin-left:4%; display: inline-block;">
                <lightning-input label="" name="file uploader" onchange={handleFilesChange} type="file" multiple></lightning-input>
            </div>
            <div style="margin-left:1%; display: inline-block;">
                <lightning-button class="slds-m-top--medium" label={UploadFile} onclick={handleSave} variant="brand" disabled={Ubtn_isTrue}></lightning-button>
            </div>
            <div style="margin-left:4%;">
                <templaet if:false={fileselected}>
                    <div class="slds-text-body_small slds-text-color_error">
                        {fileName}
                    </div>
                </templaet>
                <templaet if:true={fileselected}>
                    <div class="slds-text-body_small slds-text-color_success">
                        {fileName}
                    </div>
                </templaet>
            </div>
        </div>

        <lightning-card>
            <div style="width: auto;">
                <div if:true={dataExist}>

                    <div class="slds-grid slds-gutters">
                        <div class="slds-col" style="margin-left:4%; display: inline-block;">
                            <span>
                                <lightning-button label={buttonLabel}
                                                icon-name="utility:delete"
                                                disabled={Dbtn_isTrue}
                                                variant="destructive"
                                                onclick={deleteAccounts}></lightning-button>
                            </span>
                            <span style="margin-left: 1%">選択：
                                <span style="color:red;">{recordsCount}</span>
                            </span>
                        </div>
                    </div><br/>

                    <lightning-datatable
                            data={data}
                            columns={columns}
                            key-field="id"
                            onrowselection={getSelectedRecords}
                            resize-column-disabled>
                    </lightning-datatable>
                </div>
                <div if:false={dataExist}>
                    <div class="slds-grid slds-gutters">
                        <div class="slds-col" style="margin-left:4%;">
                            <span>登録ファイルがありません</span>
                        </div>
                    </div><br/>
                </div>
            </div>
        </lightning-card>
    </lightning-card>
</template>
```

##JavaScript
```js:attachmentFileUploader.js
import { LightningElement, track, api } from 'lwc';
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
import attachmentList from '@salesforce/apex/AttachmentController.getAttachmentList';
import delSelectedFiles from '@salesforce/apex/AttachmentController.deleteAttachments';
import saveFile from '@salesforce/apex/AttachmentController.saveFile';

const columns = [
    { label: 'ファイル名', fieldName: 'FilePreview', type: 'url',
        typeAttributes: { label: { fieldName: 'Name' }, tooltip: { fieldName: 'Name' } } },
    { label: '作成日', fieldName: 'CreatedDate', type: 'date',
        typeAttributes: { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } },
    // { label: '最終更新日', fieldName: 'LastModifiedDate', type: 'date',
    //     typeAttributes: { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' } },
    { label: '所有者', fieldName: 'OwnerName'}
];

export default class AttachmentFileUploader extends LightningElement {

    @api recordId;                             // 表示画面レコードID
    @track data;                               // 取得したAttachmentデータをHTMLに渡す
    @track columns = columns;                  // lightning-datatableに割当
    @track fileselected=false;                 // アップロード時のメッセージ表記制御
    @track recordsCount = 0;                   // 選択レコード数
    @track buttonLabel = '選択ファイルの削除';   // 削除ボタンラベル
    @track UploadFile = 'アップロード実行';      // アップロードボタンラベル
    @track dataExist = false;
    @track Ubtn_isTrue = false;                 // uploadボタンのdisabled制御
    @track Dbtn_isTrue = true;                  // deleteボタンのdisabled制御
    @track fileName = '';
    selectedRecords;
    filesUploaded = [];
    file;
    fileContents;
    fileReader;
    content;
    MAX_FILE_SIZE = 1500000;                    //ファイルサイズ制限

    /********************** 一覧表示処理 **********************/

    //画面表示時リスト
    connectedCallback() {
        this.getAttachments();
    }

    //Attachment情報取得
    getAttachments() {
        attachmentList({ parentId: this.recordId })
        .then(data => {
            console.log('data.forEach前');
            data.forEach(a =>{
                console.log('a.Owner.Name：'+ a.Owner.Name);
                a.FilePreview = '/lightning/r/' + a.Id + '/view';
                a.OwnerName = a.Owner.Name;
            });

            console.log('★data：' + JSON.stringify(data));
            this.data = data;
            if( this.data.length > 0 ){
                this.dataExist = true;
            }else{
                this.dataExist = false;
            }
        })
        .catch(error => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error - getAttachments()',
                    message: 'データ取得中にエラーが発生しました',
                    variant: 'error',
                }),
            );
        });
    }

    /********************** ファイルアップロード処理 **********************/

    //アップロードファイルの情報獲得
    handleFilesChange(event) {
        if(event.target.files.length > 0) {
            this.filesUploaded = event.target.files;
            this.fileName = event.target.files[0].name;
        }
        this.Ubtn_isTrue = false;
        this.fileselected = false;  //表示メッセージ色変更（赤）
        this.UploadFile = 'アップロード実行';
    }

    //アップロード実行ボタン押下時処理
    handleSave() {
        if(this.filesUploaded.length > 0) {
            this.UploadFile = 'Processing....';
            this.Ubtn_isTrue = true;
            this.uploadHelper();
        }
        else {
            this.fileName = 'アップロードするファイルを指定してください';
            this.fileselected = false;  //表示メッセージ色変更（赤）
        }
    }

    //サイズ判定＆登録データ生成
    uploadHelper() {
        this.file = this.filesUploaded[0];
       if (this.file.size > this.MAX_FILE_SIZE) {
            window.console.log('ファイルサイズが大きすぎます');
            this.fileName = 'ファイルサイズが大きすぎます（最大'+MAX_FILE_SIZE+'byte）';
            return ;
        }
        this.fileReader= new FileReader();
        this.fileReader.onloadend = (() => {
            this.fileContents = this.fileReader.result;
            let base64 = 'base64,';
            this.content = this.fileContents.indexOf(base64) + base64.length;
            this.fileContents = this.fileContents.substring(this.content);
            this.saveToFile();
        });

        this.fileReader.readAsDataURL(this.file);   //アップロード後画面描画
    }

    //Apex save処理
    saveToFile() {
        saveFile({ idParent: this.recordId, strFileName: this.file.name, base64Data: encodeURIComponent(this.fileContents)})
        .then(result => {
            window.console.log('result ====> ' +result);
            this.fileName = this.fileName + ' - アップロード成功';
            this.UploadFile = 'アップロード完了';
            this.fileselected = true;　 //表示メッセージ色変更（緑）
            this.getAttachments();      //リスト更新

            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Success - saveToFile()',
                    message: this.file.name + ' - ファイルアップロード処理が成功しました',
                    variant: 'success',
                }),
            );
        })
        .catch(error => {
            window.console.log(error);

            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error - saveToFile()',
                    message: 'ファイルアップロード中にエラーが発生しました',
                    //message: error.message,
                    variant: 'error',
                }),
            );
        });
    }

    /********************** ファイル削除処理 **********************/

    // ファイル情報獲得＆選択レコード合計＆ボタンdisable制御
    getSelectedRecords(event) {
        const selectedRows = event.detail.selectedRows;
        this.recordsCount = event.detail.selectedRows.length;
        let conIds = new Set();
        this.buttonLabel = '選択ファイルの削除';

        for (let i = 0; i < selectedRows.length; i++) {
            conIds.add(selectedRows[i].Id);
        }

        this.selectedRecords = Array.from(conIds);
        window.console.log('selectedRecords ====> ' +this.selectedRecords);

        if( this.recordsCount > 0 ){
            this.Dbtn_isTrue = false;
        }else{
            this.Dbtn_isTrue = true;
        }
    }

    //ファイル削除実行ボタン押下時処理
    deleteAccounts() {
        if (this.selectedRecords) {
            this.buttonLabel = 'Processing....';
            this.Dbtn_isTrue = true;
            this.deleteAttachments();
        }
    }

    //Apex delete処理
    deleteAttachments() {
        delSelectedFiles({ lstConIds: this.selectedRecords })
        .then(result => {
            window.console.log('result ====> ' + result);

            this.buttonLabel = '選択ファイルの削除完了';
            this.getAttachments();  //リフレッシュ

            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Success - deleteAttachments()',
                    message: this.recordsCount + 'ファイルを削除しました',
                    variant: 'success'
                }),
            );

            this.template.querySelector('lightning-datatable').selectedRows = [];
            this.recordsCount = 0;
        })
        .catch(error => {
            window.console.log(error);
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error - deleteAttachments()',
                    message: 'ファイル削除中にエラーが発生しました',
                    variant: 'error'
                }),
            );
        });
    }

}
```
```xml:attachmentFileUploader.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>48.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <!-- <target>lightning__AppPage</target> -->
        <!-- <target>lightning__HomePage</target> -->
    </targets>
</LightningComponentBundle>
```

##Apex
```apex:AttachmentController.cls
public with sharing class AttachmentController {

    private AttachmentController(){
    }

    @AuraEnabled
    public static list<attachment> getAttachmentList(Id parentId){
        return [
            SELECT
                Id,
                Name,
                ContentType,
                OwnerId,
                Owner.Name,
                CreatedDate,
                CreatedById,
                LastModifiedDate,
                LastModifiedById
            FROM
                Attachment
            WHERE
                ParentId = :parentId AND
                IsDeleted = false AND
                IsPrivate = false
            ORDER BY
                CreatedDate DESC
        ];
    }

    @AuraEnabled
    public static Attachment saveFile(Id idParent, String strFileName, String base64Data) {

        // Decoding base64Data
        base64Data = EncodingUtil.urlDecode(base64Data, 'UTF-8');

        // inserting file
        Attachment cv = new Attachment();
        cv.ParentId = idParent;
        cv.Name = strFileName;
        cv.Body = EncodingUtil.base64Decode(base64Data);

        Insert cv;
        return cv;
    }

    @AuraEnabled
    public static void deleteAttachments(list<Id> lstConIds){
        try {
            list<Attachment> lstConsToDelete = new list<Attachment>();
            System.debug('lstConIds ====> '+ lstConIds);
            for(Id idCon : lstConIds) {
                lstConsToDelete.add(new Attachment(Id = idCon));
            }
            if(!lstConsToDelete.isEmpty()) {
                delete lstConsToDelete;
            }
        }
        catch(Exception ex) {
            throw new AuraHandledException(ex.getMessage());
        }
    }

}
```
```apex:AttachmentController.cls-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>48.0</apiVersion>
    <status>Active</status>
</ApexClass>
```

##最後に
ここでご紹介した方法は、半ば無理やり添付ファイルを使う方法です。
お役に立てば幸いですが、早くLEXへの移行が進むといいですね！