[創意料理] 介紹一個不要臉的 jQuery 擴充函式 - jquery-model

這個不要臉的 jQuery 擴充函式 - jquery-model 是在下的拙作,原本是我個人用 jQuery 在開發前端程式時所使用的,同事也拿去用了之後受到好評,應該也可以推薦給大家,它不是一個什麼高大上的東西,只是讓我在將 UI 上的內容兜成 JSON 物件時可以少寫一些程式碼。

緣起

會搞這個東西有兩個原因,第一個原因是「有時候前端的 MVVM 框架太沉重了」,使用 MVVM 的前端框架來開發 SPA 程式真的非常合適,只要專注在 Model 上,以及事先宣告好狀態與 UI 的關係,後續就交給框架來處理,相當輕鬆,但是當我的頁面是無狀態的時候,引用 MVVM 前端框架就顯得有點殺雞用牛刀的感覺。

第二個原因是「團隊以 jQuery 為主要的前端工具」,我們公司的 UI 設計師強項在 HTML 跟 CSS,JavaScript 就弱了一些,所以經常會去找一些現成的 jQuery Plugin 回來自己調樣式,以實現頁面上一些特殊的互動功能,那麼 MVVM 前端框架大都無法跟 jQuery Plugin 直接無縫接軌,運氣好一點是可以找到有大大縫合好的版本,沒有的話就要自己縫了。

基於這兩個原因,我就在想能不能有一個簡便的方法,直接將 HTML 元件的內容轉成一個 JSON 物件,以及將 JSON 物件更新到 HTML 元件上? 因此 jquery-model 就誕生了。

適用版本

  • jquery-chefextend v2.0.0+
  • jquery-model v1.0.0+

基本用法

受到 MVVM 框架的啟發,它採用宣告式語法,有五個:c-modelc-model-stringc-model-htmlc-model-showc-model-seen(後三者稍後會介紹到),如果 property 是數值但實際是字串,就用 c-model-string,attribute 的值為 property 的名稱。

<div id="formDiv">
    <h1>formDiv</h1>
    <div><input type="text" value="abc" c-model="abcText"></div>
    <div><input type="text" value="123" c-model-string="abcNumber"></div>
    <div>
        <select value="opt2" c-model="myOpt">
            <option value="opt1">opt1</option>
            <option value="opt2">opt2</option>
            <option value="opt3">opt3</option>
        </select>
    </div>
</div>

如果要輸出 JSON 物件,就使用 model() 方法:

$("#formDiv").model();

// result: { abcText: "abc", abcNumber: "123", myOpt: "opt2" }

如果要將 JSON 物件更新到 HTML 元件上,就用 model({...})model("property", value) 方法:

$("#formDiv").model({ abcText: "cba", abcNumber: "456" });

// --or--

$("#formDiv").model("abcText", "cba");
$("#formDiv").model("abcNumber", "456");

radio

radio 是用 name attribute 組成一個群組的,所以 radio 元件必須指定 name attribute 及其群組名稱,另外 radio 群組中只需要其中一個元件宣告 c-model 或 c-model-string 即可。

<div id="formDiv">
    <h1>formDiv</h1>
    <div>
        <input type="radio" name="myRadio" value="4a" c-model="myOption" />4a
        <input type="radio" name="myRadio" value="44b" />44b
        <input type="radio" name="myRadio" value="444c" />444c
    </div>
</div>

checkbox

checkbox 如果有指定 value attribute,有勾選就會輸出 property,沒有勾選就不會;如果沒有指定 value attribute 就視為是 boolean 型別。

<div id="formDiv">
    <h1>formDiv</h1>
    <div><input type="checkbox" c-model="myChecked1" />some text</div>
    <div><input type="checkbox" value="1" c-model="myChecked2" />some text</div>
    <div><input type="checkbox" value="happy" c-model="myStatus" />some text</div>
</div>

<script>
    $("#formDiv").model();
    
    // result if all checked: { myChecked1: true, myChecked2: 1, myStatus: "happy" }
    // result if all unchecked: { myChecked1: false }
</script>

非輸入類型的 HTML 元件

如果想要顯示資料在 <p>、<div>、<span>、... 這種非輸入類型的 HTML 元件上,也是可以的,直接宣告 c-model 就好了,顯示的資料是 HTML 內容的話,則宣告 c-model-html。

<div id="formDiv">
    <h1>formDiv</h1>
    <div c-model="abcText"></div>
    <div c-model="defText"></div>
    <div c-model-html="ghiHtml"></div>
</div>

<script>
    $("#formDiv").model({ abcText: "abc", defText: "def", ghiHtml: "<h1>ghi</h1>" });
    
    // result: <div c-model="abcText">abc</div>
    //               <div c-model="defText">def</div>
    //               <div c-model-html="ghiHtml"><h1>ghi</h1></div>
</script>

由於非輸入類型的 HTML 元件互動性較低,所以在輸出成 JSON 物件時,會自動略過這些非輸入類型的 HTML 元件。

<div id="formDiv">
    <h1>formDiv</h1>
    <div><input type="text" value="aaa" c-model="aaaText"></div>
    <div c-model="abcText"></div>
    <div c-model="defText"></div>
</div>

<script>
    $("#formDiv").model();
    
    // result: { aaaText: "aaa" }
</script>

除此之外,非輸入類型的 HTML 元件在更新資料時,是可以接受 function 的。

<div id="formDiv">
    <h1>formDiv</h1>
    <div c-model="formattedAbcText"></div>
</div>

<script>
    $("#formDiv").model({ formattedAbcText: function () {
        return "formatted abc.";
    }});

    // result: <div c-model="formattedAbcText">formatted abc.</div>
</script>

而且,非輸入類型的 HTML 元件可以做多屬性賦值,語法為 c-model="key:prop,...",目前支援任何 Attribute 以及 texthtmlvaluevalue-string,舉例來說,有一個 <a> 的 href 跟 text 需要賦值給它,我們就可以這樣寫:

<div id="formDiv">
    <a c-model="href:url,text:text"></a>
</div>

<script>
    $("#formDiv").model({ url: "https://dotblogs.com.tw/supershowwei", text: "軟體主廚的程式料理廚房"});
    
    // result: <a href="https://dotblogs.com.tw/supershowwei" c-model="href:url,text:text">軟體主廚的程式料理廚房</a>
</script>

巢狀物件

實務上物件階層通常不會只有一層,jquery-model 有支援巢狀物件的取值跟賦值,範例程式碼如下:

<div id="formDiv">
    <h1>formDiv</h1>
    <div>
        <input type="text" c-model="member.name" />
        <input type="text" c-model="member.address.value" />
        <input type="text" c-model="member.phone" />
    </div>
</div>

<script>
    $("#formDiv").model({ member: { name: "Johnny", address: { value: "abc test" }, phone: "0000-00000000" } });

    var obj = $("#formDiv").model();

    // obj is { member: { name: "Johnny", address: { value: "abc test" }, phone: "0000-00000000" } }.
</script>

集合

jquery-model 是有支援集合的,如果確定 jQuery Select 出來的 HTML 元件是一集合,可以呼叫 models() 方法,輸出一個 JSON 物件的集合。

<ul id="list">
    <li>
        <div><input type="text" value="11" c-model="id"></div>
        <div><input type="text" value="22" c-model="name"></div>
        <div><input type="text" value="33" c-model="age"></div>
    </li>
    <li>
        <input type="hidden" value="44" c-model="id" />
        <div c-model="id">44</div>
        <div><input type="text" value="55" c-model="name"></div>
        <div><input type="text" value="66" c-model="age"></div>
    </li>
    <li>
        <div><input type="text" value="77" c-model="id"></div>
        <div><input type="text" value="88" c-model="name"></div>
        <div><input type="text" value="99" c-model="age"></div>
    </li>
</ul>

<script>
    $("#list > li").models();

    // result: [{ id: 11, name: "22", age: 33 }, { id: 44, name: "55", age: 66 }, { id: 77, name: "88", age: 99 }]
</script>

如果只需要取得集合中的一個,則需帶入識別用的 property 名稱及值。

$("#list > li").models("id", 11);

// result: { id: 11, name: "22", age: 33 }

再來是將集合的值更新到 HTML 元件上,使用的是 models([{...}, ...], "keyName") 方法,第一個參數是指定更新的集合,第二個參數是指定識別用的 property 名稱。

var collection = [
    { id: 11, name: "二二二", age: 33 },
    { id: 44, name: "五五五", age: 66 },
    { id: 77, name: "八八八", age: 99 }
];

$("#list > li").models(collection, "id");

如果只需要更新集合中的其中一個項目,可以使用 models({...}, "keyName") 方法。

$("#list > li").models({ id: 11, name: "二二二", age: 33 }, "id");

用 Model 產生 HTML 元件

jquery-model 的功用是 Model 與 HTML 元件 之間的取值跟賦值,沒有像是 Vue.js 或是 Angular 這種前端框架會自動產生 HTML 元件的功能,不過想要利用 Model 來產生 HTML 元件也沒有那麼難,底下是一個範例:

$("#list").models([{ id: 111, name: "111", age: 111 }, { id: 222, name: "222", age: 222 }], $("#list > li").first());

或是

var template = "<li>";
template += "<div><input type=\"text\" value=\"\" c-model=\"id\"></div>";
template += "<div><input type=\"text\" value=\"\" c-model=\"name\"></div>";
template += "<div><input type=\"text\" value=\"\" c-model=\"age\"></div>";
template += "</li>";

$("#list").models([{ id: 111, name: "111", age: 111 }, { id: 222, name: "222", age: 222 }], $(template));

beforeSet 及 afterSet 事件

在要賦予的值都 set 之前會叫用 beforeSet() 事件方法,在 set 之後會叫用 afterSet() 事件方法,範例如下:

$("#formDiv").model({ abcText: "cba", abcNumber: 456 }, function ($self, setter) {
    // ... do something on after set.
});

$("#formDiv").model({ abcText: "cba", abcNumber: 456 }, {
    beforeSet: function ($self, setter) {
        // ... do something on before set.
    },
    afterSet: function ($self, setter) {
        // ... do something on after set.
    }
});

// ---
$("#formDiv").model("abcText", "cba", function ($self, setter) {
    // ... do something on after set.
});

$("#formDiv").model("abcText", "cba", {
    beforeSet: function ($self, setter) {
        // ... do something on before set.
    },
    afterSet: function ($self, setter) {
        // ... do something on after set.
    }
});

// ---
$("#list > li").models(collection, "id", function ($self, setter) {
    // ... do something on after set.
});

$("#list > li").models(collection, "id", {
    beforeSet: function ($self, setter) {
        // ... do something on before set.
    },
    afterSet: function ($self, setter) {
        // ... do something on after set.
    }
});

// ---
$("#list > li").models({ id: 11, name: "二二二", age: 33 }, "id", function ($self, setter) {
    // ... do something on after set.
});

$("#list > li").models({ id: 11, name: "二二二", age: 33 }, "id", {
    beforeSet: function ($self, setter) {
        // ... do something on before set.
    },
    afterSet: function ($self, setter) {
        // ... do something on after set.
    }
});

// ---
$("#list").models(collection, $template, function ($self, setter) {
    // ... do something on after set.
});

$("#list").models(collection, $template, {
    beforeSet: function ($self, setter) {
        // ... do something on before set.
    },
    afterSet: function ($self, setter) {
        // ... do something on after set.
    }
});

Template Literal

從 0.2.4 開始支援樣版字串,用兩個反引號(`)括起來就被視為樣版字串,我們直接看範例。

<div id="formDiv">
    <a c-model="href:`https://dotblogs.com.tw/{url}`,text:text"></a>
    <span c-model="`https://dotblogs.com.tw/{url}`"></span>
</div>

<script>
    $("#formDiv").model({ url: "supershowwei", text: "軟體主廚的程式料理廚房"});
    
    // result:
    // <a href="https://dotblogs.com.tw/supershowwei" c-model="href:`https://dotblogs.com.tw/{url}`,text:text">軟體主廚的程式料理廚房</a>
    // <span c-model="`https://dotblogs.com.tw/{url}`">https://dotblogs.com.tw/supershowwei</span>
</script>

HTML Template Function

從 0.3.0 開始在 models() 方法中可以傳入產生 HTML 樣版的 function,下面是範例。

<ul id="list"></ul>
<ul id="template" style="display: none;">
    <li>
        <div><span c-model="id"></span></div>
        <div><span c-model="name"></span></div>
        <div><span c-model="age"></span></div>
    </li>
    <li>
        <div><input type="text" c-model="id"></div>
        <div><input type="text" c-model="name"></div>
        <div><input type="text" c-model="age"></div>
    </li>
</ul>

<script>
    $("#list")
        .models(
            [{ id: 111, name: "111", age: 111 }, { id: 222, name: "222", age: 222 }],
            function (item, index) {
                return $("#template").children().eq(index % 2);
            });
</script>

格式化 Filter

從 0.5.0 開始,可以自行增加格式化用的 Filter,而 Filter 呼叫的方法可以是 Prototype 定義的方法,也可以是任何全域的方法,我們直接來看範例。

<div id="formDiv">
    <h1>formDiv</h1>
    <div c-model="abcText|append_prefix ? '333-' & '444-'|append_suffix ? '-111' & '-222'"></div>
    <div c-model="abcNumber|.toPercent"></div>
    <div c-model="defNumber|.toPercent ? 5"></div>
    <div c-model-html="ghiHtml"></div>
</div>

<script>
    if (!Number.prototype.toPercent) {
        Number.prototype.toPercent = function (decimals) {
            return this.toLocaleString("en-US", { style: "percent", minimumFractionDigits: decimals, maximumFractionDigits: decimals });
        }
    }

    window.append_prefix = function (body, arg1, arg2) {

        if (!arg1) return body;
        if (!arg2) return arg1 + body;

        return arg2 + arg1 + body;

    }

    window.append_suffix = function (body, arg1, arg2) {

        if (!arg1) return body;
        if (!arg2) return body + arg1;

        return body + arg1 + arg2;

    }

    $("#formDiv").model({ abcText: "abc", abcNumber: 0.123456789, defNumber: 0.123456789, ghiHtml: "<h1>ghi</h1>" });
    
    // result: <div c-model="abcText|append_prefix ? '333-' & '444-'|append_suffix ? '-111' & '-222'">444-333-abc-111-222</div>
    //         <div c-model="abcNumber|.toPercent">12%</div>
    //         <div c-model="defNumber|.toPercent ? 5">12.34568%</div>
    //         <div c-model-html="ghiHtml"><h1>ghi</h1></div>
</script>

Filter 的使用方式是直接在宣告語法加上 |,Filter 可以多個,如果 Filter 方法本身是 Prototype 定義的方法,則使用 . 開頭,如範例中的 .toPercent,有需要帶入參數的話,則在 Filter 方法後面加上 △?△(△代表空格),多個參數則用 △&△ 隔開,字串參數需要用 '(單引號)括起來。

在呼叫 Filter 方法時,如果是 Prototype 定義的方法,則 Model 的值為方法的 this,如果是全域的方法,則 Model 的值永遠帶入第一個參數

支援 contenteditable 取值

從 0.5.7 開始支援從擁有 contenteditable="true" 標籤的非輸入類型 HTML 元件取值,請看以下範例:

<div id="member1">
    <div contenteditable="true" c-model="id">1</div>
    <span contenteditable="true" c-model="name">Johnny</span>
</div>

<div id="member2">
    <div contenteditable="true" c-model="text:id,value:id">2</div>
    <span contenteditable="true" c-model="text:name,value:name">Amy</span>
</div>

<script>
    var member1 = $("#member1").model();
    var member2 = $("#member2").model();
    
    // member1 is { id: 1, name: "Johnny" }
    // member2 is { id: 2, name: "Amy" }
</script>

__THIS__

從 0.5.16 開始支援 __THIS__ 語法,用 __THIS__ 可以直接綁定整個 Model 物件,但是僅限於非輸入類型 HTML 元件,也就是只能使用在顯示資料,使用範例如下:

<div id="formDiv">
    <h1>formDiv</h1>
    <div c-model="__THIS__|getProp ? 'abcTest'|append_prefix ? '333-' & '444-'|append_suffix ? '-111' & '-222'"></div>
</div>

<script>
    function getProp(obj, name) {
        return obj[name];
    }

    window.append_prefix = function (body, arg1, arg2) {

        if (!arg1) return body;
        if (!arg2) return arg1 + body;

        return arg2 + arg1 + body;

    }

    window.append_suffix = function (body, arg1, arg2) {

        if (!arg1) return body;
        if (!arg2) return body + arg1;

        return body + arg1 + arg2;

    }

    $("#formDiv").model({ abcText: "abc" });
    
    // result: <div c-model="__THIS__|getProp ? 'abcTest'|append_prefix ? '333-' & '444-'|append_suffix ? '-111' & '-222'">444-333-abc-111-222</div>
</script>

HTML 元件的顯示與隱藏

從 0.7.0 開始支援 c-model-showc-model-seen 語法,可以用來 HTML 元件的顯示或隱藏,差別在於 c-model-show 改變的是 Style 的 display 屬性,而 c-model-seen 改變的是 Style 的 visibility 屬性,請看範例:

<div id="formShow">
    <span c-model="text:abcText,show:abcText"></span>
    <div c-model-show="edfText">
        <p c-model="edfText"></p>
    </div>
</div>

<div id="formSeen">
    <span c-model="text:edfText,seen:edfText"></span>
    <div c-model-seen="abcText">
        <p c-model="abcText"></p>
    </div>
</div>

<script>
    $("#formDiv").model({ abcText: "abc", edfText: "" });
    
    // result:
    //   <div id="formShow">
    //       <span c-model="text:abcText,show:abcText">abc</span>
    //       <div style="display: none;" c-model-show="edfText">
    //           <p c-model="edfText"></p>
    //       </div>
    //   </div>
    //   
    //   <div id="formSeen">
    //       <span style="visibility: hidden;" c-model="text:edfText,seen:edfText"></span>
    //       <div c-model-seen="abcText">
    //           <p c-model="abcText">abc</p>
    //       </div>
    //   </div>
</script>

最後提醒四件事: 

  1. 儘量將 c-model、c-model-string、...等語法宣告在最後面
  2. 當應用在集合的時候,識別用的 HTML 元件必須要是輸入類型的,如果識別值不需要顯示出來,則可以考慮使用 <input type="hidden" />,然後儘量將識別用的元件放置在第一個。
  3. 在想要輸出 JSON 物件的 HTML 元件範圍內,輸入類型元件上不要重覆 property 名稱。
  4. 如果覺得這個擴充函式還可以的話,請不吝到 GitHub 給顆星星。

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學