使用VUE元件建立SpreadJS自定義單元格(一)

葡萄城技術團隊發表於2022-01-07

作為近五年都衝在熱門框架排行榜首的Vue,大家一定會學到的一部分就是元件的使用。前端開發的模組化,可以讓程式碼邏輯更加簡單清晰,專案的擴充套件性大大加強。對於Vue而言,模組化的體現集中在元件之上,以元件為單位實現模組化。

通常我們使用元件的方式是,在例項化Vue物件之前,通過Vue.component方法來註冊全域性的元件。

// 告訴Vue,現在需要元件 todo-item,配置如下,包含props和template
Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
}) 
// 例項化一個Vue物件,掛載在#app-7元素下,定它的屬性,陣列groceryList 
var app7 = new Vue({
  el: '#app-7',
  data: {
    groceryList: [
      { text: 'Vegetables' },
      { text: 'Cheese' },
      { text: 'Whatever else humans are supposed to eat' }
    ]
  }
})

在眾多元件之中,作為辦公必備的電子表格,在前端元件中也佔據了重要地位。除了以表格的形式展示資料,電子表格還有一個非常重要的功能,即支援自定義功能擴充和各種定製化的資料展示效果,比如checkbox,Radio button等;還需要實現當單元格進入編輯狀態時,使用下拉選單(或其他輸入控制元件)輸入的效果。我們稱之為"自定義單元格",一種嵌入元件內的元件。SpreadJS目前擁有8種下拉選單,在開啟列表之前,我們只需要在單元格樣式中設定選項資料。 你可以參考以下程式碼使用列表:

線上體驗地址

  // The way of click the dropdown icon to open list. 
   var style = new GC.Spread.Sheets.Style();
   style.cellButtons = [
       {
           imageType: GC.Spread.Sheets.ButtonImageType.dropdown,
           command: "openList",
           useButtonStyle: true,
       }
   ];
   style.dropDowns = [
       {
           type: GC.Spread.Sheets.DropDownType.list,
           option: {
               items: [
                   {
                       text: 'item1',
                       value: 'item1'
                   },
                   {
                       text: 'item2',
                       value: 'item2'
                   },
                   {
                       text: 'item3',
                       value: 'item3'
                   },
                   {
                       text: 'item4',
                       value: 'item4'
                   }
               ],
           }
       }
   ];
   sheet.setText(2, 1, "Vertical text list");
   sheet.setStyle(3, 1, style);

   // The way open list with command rather then clicking the dropdown button.
   spread.commandManager().execute({cmd:"openList",row:3,col:1,sheetName:"Sheet1"});

前端電子表格固然好用, 但由於框架生命週期以及自定義單元格渲染邏輯的問題,目前的技術手段無法直接在框架頁面下直接通過template的方式使用框架下的元件。在之前的內容中,我們提到了可以使用Svelte使用Web Conmponents封裝其他元件可以使用的元件。
除了上面提到的方法之外,我們如果想在Vue環境下使用自定義單元格,可以考慮使用持動態渲染的方式來建立和掛載元件,從而將元件注入自定義單元格。

下面為大家演演示如何在VUE專案中,建立一個使用VUE 元件的自定義單元格。

實踐

首先,在專案中開啟執行時載入,在vue.config.js中新增runtimeCompiler: true。

    module.exports = {
        devServer: {
            port: 3000
        },
        <font color="#ff0000">runtimeCompiler: true</font>
      }

引用ElementUI,需要注意要把element 的css引用放在APP import前,這樣修改樣式,才能覆蓋原有專案內容。

import Vue from 'vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue'
import router from './router'

Vue.use(ElementUI);

new Vue({
  el: '#app',
  router,
  render: h => h(App)
})

Vue.config.productionTip = false

建立AutoComplateCellType,具體程式碼如下,需要注意幾點。
1、自定義的元素,需要新增gcUIElement屬性,如果元素或者其父元素沒有該屬性,點選建立的元件便會直接退出編輯狀態無法編輯。
對於ElementUI 的autocomplete,預設下拉選項內容是注入到body中的,需要給元件模板中設定:popper-append-to-body="false",讓彈出的下拉選項在gcUIElement的Div中渲染。
如果使用其他元件沒有類似選項,也可以跟進實際情況在彈出時在新增gcUIElement屬性。
2、使用動態掛載元件的 this.vm 設定和獲取單元格的值。
3、在deactivateEditor中銷燬元件。

import Vue from 'vue'
import * as GC from "@grapecity/spread-sheets"
import DataService from './dataService'

function AutoComplateCellType() {
}
AutoComplateCellType.prototype = new GC.Spread.Sheets.CellTypes.Base();
AutoComplateCellType.prototype.createEditorElement = function (context, cellWrapperElement) {
  cellWrapperElement.style.overflow = 'visible'
  let editorContext = document.createElement("div")
  editorContext.setAttribute("gcUIElement", "gcEditingInput");
  let editor = document.createElement("div");
  // 自定義單元格中editorContext作為容器,需要在建立一個child用於掛載,不能直接掛載到editorContext上
  editorContext.appendChild(editor);
  return editorContext;
}
AutoComplateCellType.prototype.activateEditor = function (editorContext, cellStyle, cellRect, context) {
    let width = cellRect.width > 180 ? cellRect.width : 180;
    if (editorContext) {
        
        // 動態建立VUE 元件並掛載到editor
        const AutoCompleteComponent = {
            props: ['text','cellStyle'],
            template: `<div>
                        <el-autocomplete
                        :style="cellStyle"
                        popper-class="my-autocomplete"
                        v-model="text"
                        :fetch-suggestions="querySearch"
                        placeholder="請輸入內容"
                        :popper-append-to-body="false"
                        value-key="name"
                        @select="handleSelect">
                        <i class="el-icon-edit el-input__icon"
                            slot="suffix"
                            @click="handleIconClick">
                        </i>
                        <template slot-scope="{ item }">
                            <div class="name">{{ item.name }}</div>
                            <span class="addr">{{ item.phone }}</span>
                        </template>
                        </el-autocomplete>
                    </div>`,
            mounted() {
                this.items = DataService.getEmployeesData();
            },
            methods: {
                querySearch(queryString, cb) {
                    var items = this.items;
                    var results = queryString ? items.filter(this.createFilter(queryString)) : items;
                    // 無法設定動態內容的位置,可以動態新增gcUIElement
                    // setTimeout(() => {
                    //   let popDiv = document.getElementsByClassName("my-autocomplete")[0];
                    //   if(popDiv){
                    //     popDiv.setAttribute("gcUIElement", "gcEditingInput");
                    //   }
                    // }, 500);
                    // 呼叫 callback 返回建議列表的資料
                    cb(results);
                },
                createFilter(queryString) {
                    return (restaurant) => {
                    return (restaurant.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
                    };
                },
                handleSelect(item) {
                    console.log(item);
                },
                handleIconClick(ev) {
                    console.log(ev);
                }
            }
        };

      // create component constructor
      const AutoCompleteCtor = Vue.extend(AutoCompleteComponent);
      this.vm = new AutoCompleteCtor({
        propsData: {
          cellStyle: {width: width+"px"}
        }
      }).$mount(editorContext.firstChild);
    }
    return editorContext;
};
AutoComplateCellType.prototype.updateEditor = function(editorContext, cellStyle, cellRect) {
    // 給定一個最小編輯區域大小
    let width = cellRect.width > 180 ? cellRect.width : 180;
    let height = cellRect.height > 40 ? cellRect.height : 40;
    return {width: width, height: height};
};
AutoComplateCellType.prototype.getEditorValue = function (editorContext) {
    // 設定元件預設值
    if (this.vm) {
        return this.vm.text;
    }
};
AutoComplateCellType.prototype.setEditorValue = function (editorContext, value) {
    // 獲取元件編輯後的值
    if (editorContext) {
      this.vm.text = value;
    }
};
AutoComplateCellType.prototype.deactivateEditor = function (editorContext, context) {
    // 銷燬元件
    this.vm.$destroy();
    this.vm = undefined;
};

export {AutoComplateCellType};

效果如圖:

一個完美的單元格新鮮出爐~

這裡介紹的方式只是諸多實現方案的一種。如果大家有其他更好的想法方法,歡迎一起討論 ~

如果你對其他更多前端電子表格中有趣功能感興趣,可以檢視 SpreadJS更多例項演示

我們也會在之後,持續為大家帶來更多帶來更多嚴肅和有趣的內容 ~

相關文章