這兩年我的網頁開發主要走「輕前端」路線,以 ASP.NET MVC 或 ASP.NET Core .cshtml 為主體,直接 <script > 載入 jQuery、KendoUI 等程式庫處理 UI,若元素間互動較複雜的則用 Vue.js 實踐 MVVM。這有些背離當代重度依賴 npm、webpack、TypeScript 等編譯程序的主流前端開發方式,但它簡單輕巧、學習門檻低,對於具備 ASP.NET WebForm 背景的開發人員來說,比從頭學習整套前端框架及截然不同開發程序好上手很多。(我一直覺得 React.js/Angular 這類大框架需要「對它有愛」才容易上手,而且這還蠻吃緣份的,不是人人都能修成正果)

在 Kendo UI 與 Vue.js 的 MVVM 整合上,我一直有個小困擾。之前 Knockout.js 時代有現成的 Knockout-Kendo.js協助處理資料繫結,AngularJS 時代則有 angular-kendo.js。到了今天,Kendo UI 官方已直接支援 Vue.js,可以直接寫成:

<div id="vueapp" class="vue-app">
    <h5>Select date: </h5>
    <kendo-datepicker :min="minDate"
                      :max="maxDate"
                      :value="currentDate"
                      :format="'yyyy/MMMM/dd'"></kendo-datepicker>
</div>

但有個問題,要用它需回歸主流前端開發方式 - npm install、import Packages,再用 webpack 編譯的那一整套做法,我想走輕前端這條路,得自己想辦法。之前我的處理方式是 Kendo UI 輸入部分不繫結到 Vue 屬性,另寫 $("#txtDateInput").data("kendoDatePicker").value() 抓值,下場是 Vue 方法會不時出現 $("#...").data("kendoXXXX"),雖然用起來沒什麼問題,但一來程式碼變得雜亂,二來是要雙向繫結得加寫一堆邏輯,有點回到沒有 MVVM 前的蠻荒時代。

Vue.js Component 允許你寫出 <v-kendo-date-picker> 這類自訂元件,轉換產生的 HTML 元素、繫結屬性、事件全部都能客製,這篇文章就以 Kendo UI DatePicker 為例,來寫一個支援 v-model 雙向繫結的 Kendo UI 日期選擇器元件。

Vue.js 的 Component 說明文件寫得蠻清楚的,簡單整理本次案例相關重點:

  1. 使用 Vue.component('component-element-name', ) 註冊元件, 的內容跟 new Vue() 差不多,一樣有 data、watch、computed、methods 等(當然,沒有 el)。
  2. Component 的 data 必須為函式 data: function() { return { propName: propValue }; } (確保重複使用該 Component 時能各自有自己的 data)
  3. 使用 props 定義跟外界溝通的屬性,有兩種格式:1) props: ['attr1', 'attr2'] 2) props: { attr1: String, attr2: Boolean, attr3: Number }
  4. template 為產生 HTML 元素的樣版,其中的 v-bind、v-on 與一般 Vue 寫法相同
  5. Vue Component 可實現 v-model="..." 雙向繫結,方法是定義一個 prop: ['value'] 接受外部傳入值,傳值出去時則呼叫 this.$emit('input', theValue)。
  6. mounted、destroy,用來放 HTML 元素產生後的設定邏輯及消滅前的清理工作

掌握以上技巧,我寫了一個簡單範例,自訂 <v-kendo-date-picker> 元件以建立日期或日期選擇器,同時支援 KendoDatePicker 及 KendoDateTimePicker (由 Attribute time-picker="true" 切換):線上展示

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>KendoDataPicker Vue Component</title>
    <link href="https://da7xgjtj801h2.cloudfront.net/2015.2.624/styles/kendo.common.min.css" rel="stylesheet"
        type="text/css" />
    <link href="https://da7xgjtj801h2.cloudfront.net/2015.2.624/styles/kendo.silver.min.css" rel="stylesheet"
        type="text/css" />

    <style>
        html,
        body {
            font-size: 9pt;
        }

        [v-cloak] {
            display: none;
        }
    </style>
</head>

<body>
    <div id="app" v-cloak>
        <fieldset>
            <legend>KendoDatePicker</legend>
            <v-kendo-date-picker v-model="SelDate"></v-kendo-date-picker> 
            <div>{{SelDate.toLocaleString()}}</div>
        </fieldset>
        <fieldset>
            <legend>KendoDateTimePicker</legend>
            <v-kendo-date-picker v-model="SelDateTime" time-picker="true"></v-kendo-date-picker> 
            <div>{{SelDateTime.toLocaleString()}}</div>
        </fieldset>
    </div>
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script src="https://da7xgjtj801h2.cloudfront.net/2015.2.624/js/kendo.ui.core.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
    <script>
        //實際應用時,元件會另存成 vue-components-filename.js 方便重複利用
        Vue.component("v-kendo-date-picker", {
            props: ['value', 'time-picker'],
            data: function() {
                return {
                    kendoObject: null
                };
            },
            template: '<input type="text" />',
            watch: {
                value: function (val) {
                    this.kendoObject.value(val);
                }
            },
            mounted: function () {
                var self = this;
                var format = self.timePicker ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd";
                var widgetName = self.timePicker ? "kendoDateTimePicker" : "kendoDatePicker";
                $(self.$el)[widgetName]({
                    value: self.value,
                    format: format,
                    change: function () {
                        self.$emit('input', this.value());
                    }
                });
                self.kendoObject = $(self.$el).data(widgetName);
            },
            destroy: function () {
                this.kendoObject && this.kendoObject.destroy();
            }
        });
    </script>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                SelDate: kendo.date.today(),
                SelDateTime: new Date()
            }
        });
    </script>
</body>

</html>

測試成功! 日期選擇器的初始值由 SelDate 及 SelDateTime 屬性決定,選取結果也會即時反映到 SelDate 及 SelDateTime 屬性上。

學會寫 Vue Component 後,我打算把一些常用的輸入欄位包成元件,未來再陸續分享。

Vue.js component turtorial and an example of KendoDatePicker warpper.


Comments

# by Milkker

這方法我有有用過,不過 component 一多會變很雜 後來是透過 partial view 管理每個 component 的註冊。

# by Jeffrey

to Milkker,是指把 View.component() 寫在 Partial View,一個元件一個 cshtml 嗎?

# by Lik

我的想法和黑大一樣。 後端產生的cshtml, 加上 vue 帶來的MVVM的好處。 但是,如果用vue.js 想要重用component的話,我看到一面倒的是 vue 單文件做法,一個component一個vue文件。 最終難免要進入到npm, vue/cli。然後被逼到了SPA。 然後就是這個SPA 怎麼和 asp.net mvc才能結合呢。。。看到網上大部分人的說法就是捨棄 razor cshtml,用 asp.net webapi + vue.js。 似乎路是偏離得原來想法越來越遠。

# by Milkker

對呀,讓每個組件有自己的 partial view _counter.chtml <script type="text/x-template" id="counterTemplate"> <span>{{ Count }}</span> </script> <script> Vue.component('counter', { template: '#counterTemplate', data: function() { return { Count: 0 }; } }); </script>

Post a comment