Vue.jsとJSforceでSalesforce（Visualforce）開発

## Pure Javascript と Vue.js の実装比較

Lightning とか良く知らないので Visualforce での実装比較。過去に開発したシステムを Vue で書き換える機会があったので共有します。

### Pure Javascript + JSforce での実装

無償でやってる知り合いの工場の倉庫のシステムで権利は私になるのでまんま掲載。なのでバーコード読み取る前提とかちょっと変なとこあります。

```html

<apex:page lightningStylesheets="true">
<apex:slds />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsforce/1.9.1/jsforce.min.js"></script>
<script>
  var conn = new jsforce.Connection({ accessToken: '{!$API.Session_Id}' });

  window.onload = function() {
    document.getElementById('yarnInput').focus();
  }
  
  function addYarnList(v) {
    if (v.length == 8) {
      var input = document.getElementById('yarnInput');
      var ul = document.getElementById('yarnList');
      var li = document.createElement('li');
      li.textContent = v;
      li.className = 'slds-list__item';
      li.style = 'padding:10px';
      li.appendChild(createLotInput());
      li.appendChild(createYarnMasterInput());
      li.appendChild(createWeightInput());
      li.appendChild(createRemoveButton());
      ul.appendChild(li);
      allocate();
      input.value = "";
    }
  }

  function allocate() {
    var lotInput = document.getElementById('lotInput').value;
    var yarnMasterInput = document.getElementById('yarnMasterInput').value;
    var weightInput = document.getElementById('weightInput').value || 0;
    var ul = document.getElementById('yarnList');
    var yarnWeight = Math.floor(weightInput / ul.children.length * 100) / 100;
    if (ul.children.length > 0) {
      for (var i = 0; i < ul.children.length; i++) {
        ul.children[i].children[0].value = lotInput;
        ul.children[i].children[1].value = yarnMasterInput;
        if (weightInput == 0) {
          document.getElementById('weightInput').value = 0;
          ul.children[i].children[2].value = 0;
        } else {
          if (i == ul.children.length - 1) {
            ul.children[i].children[2].value = Math.round((Number(weightInput) - yarnWeight * (ul.children.length - 1)) * 100) / 100;
          } else {
            ul.children[i].children[2].value = yarnWeight;
          }
        }
      }
    }
  }

  function createLotInput() {
    var lotInput = document.getElementById('lotInput').value;
    var input = document.createElement('input');
    input.type = 'text';
    input.style = 'margin-left:10px';
    if (lotInput != '') {
      input.value = lotInput;
    }
    return input;
  }

  function createYarnMasterInput() {
    var yarnMasterInput = document.getElementById('yarnMasterInput').value;
    var input = document.createElement('input');
    input.type = 'text';
    if (yarnMasterInput != '') {
      input.value = yarnMasterInput;
    }
    return input;
  }

  function createWeightInput() {
    var input = document.createElement('input');
    input.type = 'text';
    return input;
  }

  function createRemoveButton() {
    var input = document.createElement('input');
    input.type = 'button';
    input.value = '削除';
    input.onclick = function(e) {
      var li = e.target.parentNode;
      li.parentNode.removeChild(li);
      allocate();
    };
    return input;
  }

  function register() {
    var records = [];
    var lotId, masterId;
    var queryString = "SELECT Id, Name, Lot__c, YarnMaster__c, Weight__c FROM Yarn__c WHERE ";
    var lotInput = document.getElementById('lotInput').value;
    var yarnMasterInput = document.getElementById('yarnMasterInput').value;
    var li = document.getElementById('yarnList').children;
    document.getElementById('spinner').style.display = 'block';
    for (var i = 0; i < li.length; i++) {
      queryString += "Name = '" + li[i].textContent + "' OR ";
      records[li[i].textContent] = li[i].children[2].value;
    }
    conn.sobject('Lot__c')
      .find({ 'Name': lotInput })
      .then(function (rets) {
        lotId = rets[0].Id
        return conn.sobject('YarnMaster__c').find({ 'Name': yarnMasterInput })
      })
      .then(function (rets) {
        masterId = rets[0].Id
        return conn.query(queryString.slice(0, -4))
      })
      .then(function (rets) {
        for (var i = 0; i < rets.records.length; i++) {
          rets.records[i].Lot__c = lotId;
          rets.records[i].YarnMaster__c = yarnMasterId;
          rets.records[i].Weight__c = records[rets.records[i].Name];
          delete rets.records[i].Name;
        }
        return conn.sobject('Yarn__c').update(rets.records, { allOrNone: true })
      })
      .then(function (rets) {
        document.getElementById('yarnList').innerHTML = '';
        document.getElementById('spinner').style.display = 'none';
        for (var i = 0; i < rets.length; i++) {
          if (rets[i].success) {
            console.log("Updated Successfully : " + rets[i].id);
          }
        }
      }, function (err) {
        console.err(err)
      })
  }
</script>
<div class="slds-scope">
  <div id="spinner" class="demo-only demo-only demo-only_viewport demo--inverse" style="display:none">
    <div class="slds-spinner_container slds-is-fixed">
      <div role="status" class="slds-spinner slds-spinner_medium">
        <span class="slds-assistive-text">Loading</span>
        <div class="slds-spinner__dot-a"></div>
        <div class="slds-spinner__dot-b"></div>
      </div>
    </div>
  </div>
  <div class="slds-page-header">
    <div class="slds-page-header__row">
      <div class="slds-page-header__col-title">
        <div class="slds-media">
          <div class="slds-media__figure">
            <span class="slds-icon_container slds-icon-standard-opportunity" title="register">
              <svg class="slds-icon slds-page-header__icon" aria-hidden="true">
              </svg>
              <span class="slds-assistive-text">register</span>
            </span>
          </div>
          <div class="slds-media__body">
            <div class="slds-page-header__name">
              <div class="slds-page-header__name-title">
                <h1>
                  <span class="slds-page-header__title slds-truncate" title="糸登録">糸登録</span>
                </h1>
              </div>
            </div>
            <p class="slds-page-header__name-meta">紙管のQRコードを読み取って一括登録します</p>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="slds-form--inline" style="margin:10px">
    <div class="slds-form-element">
      <div class="slds-form-element__control">
        <input type="text" id="yarnInput" class="slds-input" onKeyUp="addYarnList(this.value)" / >
      </div>
    </div>
    <div class="slds-form-element">
        <label class="slds-form-element__label" for="name">ロット</label>
        <div class="slds-form-element__control">
          <input type="text" id="lotInput" class="slds-input" onChange="allocate()" / >
        </div>
      </div>
      <div class="slds-form-element">
          <label class="slds-form-element__label" for="name">マスタ</label>
          <div class="slds-form-element__control">
            <input type="text" id="yarnMasterInput" class="slds-input" onChange="allocate()" / > 
          </div>
        </div>
    <div class="slds-form-element">
      <label class="slds-form-element__label" for="email">重量</label>
      <div class="slds-form-element__control">
        <input type="text" id="weightInput" value="0" class="slds-input" onChange="allocate()" />
      </div>
    </div>
    <div class="slds-form-element">
      <input type="button" value="登録" class="slds-button slds-button--brand" onClick="register()" />
    </div>
  </div>
  <ul id="yarnList" style="margin:10px" class="slds-list--vertical slds-has-cards slds-has-block-links--space slds-has-list-interactions"  />
</div>
</apex:page>
```

### Vue.js + JSforce での実装

Visualforce で `v-focus` みたいな Vue のカスタムディレクティブは通らないのでルートインスタンスは DOM にマウントするだけにします。実際の処理はすべてコンポーネントにします。

コンポーネントを別ファイルにして読み込むのも面倒なので全部1枚の Visualforce Page に書きます。

```html
<apex:page lightningStylesheets="true">
<apex:slds />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsforce/1.9.1/jsforce.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
  <yarn-register></yarn-register>
</div>
<script>
  var conn = new jsforce.Connection({ accessToken: '{!$API.Session_Id}' })

  Vue.component('yarn-item', {
    props: ['yarn', 'index', 'remove'],
    template: `
      <li class="slds-list__item" style="padding:10px">
        {{ yarn.id }}
        <input type="text" style="margin-left:10px" :value=yarn.lot />
        <input type="text" :value=yarn.master />
        <input type="text" :value=yarn.weight />
        <input type="button" v-on:click="remove(index)" value="削除" />
      </li>
    `
  })

  Vue.component('yarn-register', {
    data: function () {
      return {
        loading: false,
        header: {
          title: '糸登録',
          description: '紙管のQRコードを読み取って一括登録します'
        },
        yarnInput: '',
        lotInput: '',
        masterInput: '',
        weightInput: 0,
        yarnList: []
      }
    },
    methods: {
      add: function () {
        if (this.yarnInput.length == 8) {
          this.yarnList.push({ 
            id: this.yarnInput
          })
          this.allocate()
          this.yarnInput = ''
        }
      },
      remove: function (index) {
        this.yarnList.splice(index, 1)
        this.allocate()
      },
      allocate: function () {
        var yarnList = this.yarnList
        var yarnWeight = Math.floor(this.weightInput / yarnList.length * 100) / 100
        for (var i = 0; i < yarnList.length; i++) {
          if (i == yarnList.length - 1) {
            yarnWeight = Math.round((Number(this.weightInput) - yarnWeight * (yarnList.length - 1)) * 100) / 100
          }
          this.yarnList.splice(i, 1, {
            id: yarnList[i].id,
            lot: this.lotInput,
            master: this.masterInput,
            weight: yarnWeight
          })
        }
      },
      register: function () {
        var updateRets = []
        var lotId, masterId
        var yarnList = this.yarnList
        var queryString = "SELECT Id, Name, Lot__c, YarnMaster__c, Weight__c FROM Yarn__c WHERE "
        for (var i = 0; i < yarnList.length; i++) {
          queryString += "Name = '" + yarnList[i].id + "' OR "
          updateRets[yarnList[i].id] = yarnList[i].weight
        }
        this.loading = true
        conn.sobject('Lot__c')
          .find({ 'Name': this.lotInput })
          .then(function (rets) {
            lotId = rets[0].Id
            return conn.sobject('YarnMaster__c').find({ 'Name': this.masterInput })
          })
          .then(function (rets) {
            masterId = rets[0].Id
            return conn.query(queryString.slice(0, -4))
          })
          .then(function (rets) {
            for (var i = 0; i < rets.records.length; i++) {
              rets.records[i].Lot__c = lotId
              rets.records[i].YarnMaster__c = masterId
              rets.records[i].Weight__c = updateRets[rets.records[i].Name]
              delete rets.records[i].Name
            }
            return conn.sobject('Yarn__c').update(rets.records, { allOrNone: true })
          })
          .then(function (rets) {
            for (var i = 0; i < rets.length; i++) {
              if (rets[i].success) {
                console.log("Updated Successfully : " + rets[i].id)
              }
            }
          }, function (err) {
            console.err(err)
          })
          Object.assign(this.$data, this.$options.data())
      }
    },
    directives: {
      focus: {
        inserted: function (el) {
          el.focus()
        }
      }
    },
    template: `
      <div class="slds-scope">
        <div class="demo-only demo-only demo-only_viewport demo--inverse" v-if="loading">
          <div class="slds-spinner_container slds-is-fixed">
            <div role="status" class="slds-spinner slds-spinner_medium">
              <span class="slds-assistive-text">Loading</span>
              <div class="slds-spinner__dot-a"></div>
              <div class="slds-spinner__dot-b"></div>
            </div>
          </div>
        </div>
        <div class="slds-page-header">
          <div class="slds-page-header__row">
            <div class="slds-page-header__col-title">
              <div class="slds-media">
                <div class="slds-media__figure">
                  <span class="slds-icon_container slds-icon-standard-opportunity"  v-bind:title="header.title">
                    <svg class="slds-icon slds-page-header__icon" aria-hidden="true"></svg>
                    <span class="slds-assistive-text">{{ header.title }}</span>
                  </span>
                </div>
                <div class="slds-media__body">
                  <div class="slds-page-header__name">
                    <div class="slds-page-header__name-title">
                      <h1>
                        <span class="slds-page-header__title slds-truncate" v-bind:title="header.title">{{ header.title }}</span>
                      </h1>
                    </div>
                  </div>
                  <p class="slds-page-header__name-meta">{{ header.description }}</p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="slds-form--inline" style="margin:10px">
          <div class="slds-form-element">
            <div class="slds-form-element__control">
              <input type="text" class="slds-input" v-model="yarnInput" v-on:keyup="add" v-focus />
            </div>
          </div>
          <div class="slds-form-element">
            <label class="slds-form-element__label" for="name">ロット</label>
            <div class="slds-form-element__control">
              <input type="text" class="slds-input" v-model="lotInput" v-on:change="allocate" />
            </div>
          </div>
          <div class="slds-form-element">
            <label class="slds-form-element__label" for="name">マスタ</label>
            <div class="slds-form-element__control">
              <input type="text" class="slds-input" v-model="masterInput" v-on:change="allocate" /> 
            </div>
          </div>
          <div class="slds-form-element">
            <label class="slds-form-element__label" for="email">重量</label>
            <div class="slds-form-element__control">
              <input type="text" value="0" class="slds-input" v-model="weightInput" v-on:change="allocate" />
            </div>
          </div>
          <div class="slds-form-element">
            <input type="button" value="登録" class="slds-button slds-button--brand" v-on:click="register" />
          </div>
        </div>
        <ul style="margin:10px" class="slds-list--vertical slds-has-cards slds-has-block-links--space slds-has-list-interactions">
          <yarn-item
            v-for="(yarn, index) in yarnList"
            v-bind:yarn="yarn" 
            v-bind:index="index" 
            v-bind:key="yarn.id" 
            v-bind:remove="remove"
          ></yarn-item>
        </ul>
      </div>
    `
  })

  new Vue({ el: '#app' })
</script>
</apex:page>
```

### 所感

* Salesforce + Vue.js のエントリあまりない（ React 多い)
* Visualforce タブで画面遷移するなら Vue の方が解りやすい気がする
* Salesforce のカスタマイズ開発に大きなJSフレームワークは要らない気がする