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