jQuery – vmodel.js 討論列表範例教學(2) 添加回覆功能
上一則提到我們試著用 vmodel 做出討論列表,但如果需要讓使用者,針對每一則留言去回覆,那就會更加複雜了。沒關係,我們試試如何透過 vmodel 解決這個複雜的介面設計。vmodel 試圖解決一個明確問題就是:程式碼容易閱讀。解決複雜的介面設計,不代表要用什麼特殊的、另類的、炫麗的技巧來完成,能用最簡單的方式解決困難的問題,是 vmodel() 要做的事情。
比對舊版面與新版面
(舊款)
(新款)
我們可以看到,這次新的版面多多出「回覆列表 + 回覆表單」。回覆列表的每則訊息方塊(黃色姓名的一則一則),都叫做 .comment ,HTML 其實都是直接 copy 原本就有的編碼。只是透過CSS匯了一些樣式。所以我們工程師與設計師溝通,就知道 .comment 的模組勢必可以重複使用。
拿到版面,我們應該會看到靜態HTML編碼長這樣
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Vmodel</title> <link rel="stylesheet" href="index2.css"><!-- 範例樣式 --> </head> <body> <div class="box"> <!-- form --> <div class="form"> <form class="userdata" action="" data-user-name="小明"> <input type="text" class="text"> <button class="submit">送出</button> </form> </div> <!-- list --> <div class="list"> <!-- comment --> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> <!-- 新增的 reply 開始 --> <div class="box_reply"> <div class="list_reply"> <!-- 這裡的 .comment 與上方的 .comment 一模一樣喔 --> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> </div> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> </div> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> </div> </div> <form class="form_reply" data-user-name="Lee"> <textarea class="text" placeholder="回覆訊息..."></textarea> <button class="submit_reply">送出</button> </form> </div> <!-- 新增的 reply 結束 --> </div> <div class="comment"> <span class="name">Maple</span> <span class="say">範例文字範例文字範例文字</span> - <span class="current">下午3:14:50</span> </div> <div class="comment"> <span class="name">Ghost</span> <span class="say">範例文字範例文字</span> - <span class="current">下午3:14:50</span> </div> </div> </div> <!-- 提示訊息顯示 --> <div class="message"> <div class="title">Message</div> <div class="liwrap"> <div class="comment"> <span class="name">小華</span> <span class="say">哈囉!</span> - <span class="current">下午3:15:39</span> </div> <div class="comment"> <span class="name">小華</span> <span class="say">哈囉!</span> - <span class="current">下午3:15:39</span> </div> <div class="comment"> <span class="name">小華</span> <span class="say">哈囉!</span> - <span class="current">下午3:15:39</span> </div> </div> </div> </body> </html>
好的,太多不想看沒關係,我們看這次新增的重點。位在原本 .comment 底下的最後面,增加了 .box_reply
<!-- comment --> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> <!-- 新增的 reply 開始 --> <div class="box_reply"> <div class="list_reply"> <!-- 這裡的 .comment 與上方的 .comment 一模一樣喔 --> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> </div> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> </div> <div class="comment"> <span class="name">Jason</span> <span class="say">範例文字</span> - <span class="current">下午3:14:50</span> </div> </div> <form class="form_reply" data-user-name="Lee"> <textarea class="text" placeholder="回覆訊息..."></textarea> <button class="submit_reply">送出</button> </form> </div> <!-- 新增的 reply 結束 --> </div>
我們簡述它的結構表示會長這樣
.box .form .list .comment .name .say .current .box_reply // 從這裡開始,是新添加的回覆模組 .list_reply // 放置多筆回覆討論的列表 .comment //這裡是每一則回覆的留言,結構與原本的 .comment 一樣 .name .say .current ~ .form_reply //回覆表單 .text // 使用者輸入的文字 .submit_reply //送出按鈕 ~
好的,接著我們要支解這些繼承關係,變成彼此互不關聯的編碼。所以整個架構會變成整樣
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Vmodel</title> <style></style> <script src="//code.jquery.com/jquery-1.11.3.min.js"></script><!-- jQuery 核心 --> <script src="../src/jquery.vmodel.min.js"></script><!-- vmodel 核心 --> <script src="index2.js"></script><!-- 範例編碼 --> <link rel="stylesheet" href="index2.css"><!-- 範例樣式 --> </head> <body> <!-- 預期的結構長這樣 .box .form .list .comment .name .say .current .box_reply // 從這裡開始,是新添加的回覆模組 .list_reply // 放置多筆回覆討論的列表 .comment //這裡是每一則回覆的留言,結構與原本的 .comment 一樣 .name .say .current ~ .form_reply //回覆表單 .text // 使用者輸入的文字 .submit_reply //送出按鈕 ~ --> <!-- 四種模組的編碼 --> <!-- box --> <div class="box"></div> <!-- form --> <div class="form"> <form class="userdata" action="" data-user-name="小明"> <input type="text" class="text" autofocus> <button class="submit">送出</button> </form> </div> <!-- list --> <div class="list"></div> <!-- comment 預先隱藏起來 --> <div class="comment" hidden> <span class="name"></span> <span class="say"></span> - <span class="current"></span> </div> <!-- 提示訊息顯示 --> <div class="message"> <div class="title">Message</div> <div class="liwrap"></div> </div> <!-- 以下是新增支解的 3 個模組, 其中 .comment 我們會使用上面已經有的模組做後續的程式擴充 --> <!-- reply --> <div class="box_reply" hidden> </div> <!-- list --> <div class="list_reply"> </div> <!-- form --> <form class="form_reply" data-user-name="Lee" hidden> <textarea class="text" placeholder="回覆訊息..."></textarea> <button class="submit_reply">送出</button> </form> </body> </html>
我們細看這次添加的重點位置
<!-- 以下是新增支解的 3 個模組, 其中 .comment 我們會使用上面已經有的模組做後續的程式擴充 --> <!-- reply --> <div class="box_reply" hidden> </div> <!-- list --> <div class="list_reply"> </div> <!-- form --> <form class="form_reply" data-user-name="Lee" hidden> <textarea class="text" placeholder="回覆訊息..."></textarea> <button class="submit_reply">送出</button> </form>
開始修改程式
我們整個修改完的會是這樣
$(function (){ // 表單模組 $(".form").vmodel("md/form", false, function (){ var vs = this; this.autoload = ['init_position', 'submit']; // 初始化會被搬移到 .box 底下 this.init_position = function (){ vs.root.appendTo(".box"); } // 取得輸入的文字 this.user_say = function (){ var val = vs.root.find(".text").val(); return $.trim(val); } // 取得使用者是誰 this.user_name = function (){ return $.trim(vs.root.find(".userdata").attr("data-user-name")); } // 負責放到 .comment this.put = function (user, say, callback){ $.vmodel.get("md/comment").say(user, say); if (callback) callback(); } //送出時... this.submit = function (){ vs.root.on("submit", ".userdata", function (){ var user = vs.user_name(); var say = vs.user_say(); vs.put(user, say, function (){ //發送後可以做一些事情... //將回覆框放進來 var where = $(".list .comment").first(); $.vmodel.get("md/box_reply").post_to(where) }); return false; }) } // 清空 this.clean = function (){ vs.root.find(".text").val(null); } }); // 列表模組 $(".list").vmodel("md/list", false, function (){ var vs = this; this.autoload = ['init_position', 'when_reply', 'when_send_reply']; // 這裡只負責推送到 .box ,為了範例簡單,我們這裡不推到 message。 this.init_position = function (){ vs.root.appendTo(".box"); } // 當使用者想要回覆該則留言 this.when_reply = function() { vs.root.on("click", ".comment .say", function (){ //顯示該則的回覆表單 var who = $(this).parents(".comment"); $.vmodel.get("md/form_reply").show(who); }); } //當送出回覆表單。要綁定在這裡,而不是綁定在 md/comment ,是因為 .list 是不會變動的。 this.when_send_reply = function (){ vs.root.on("submit", ".form_reply", function (){ $.vmodel.get("md/form_reply").send(this); return false; }); } }); // 整體框的主要模組 $(".box").vmodel("md/box", false, function (){ var vs = this; this.autoload = ['init']; this.init = function (){ //只放置表單、與列表框到指定的位置。 //目前 .list 應該不會有任何資料,一直到使用者送出表單。 vs.create_form() .create_list(); } //初始化使用者表單 this.create_form = function (){ $.vmodel.get("md/form", true); return vs; } //初始化列表 this.create_list = function (){ $.vmodel.get("md/list", true); } }); //討論模組 $(".comment").vmodel("md/comment", false, function (){ var vs = this; // 使用者說了什麼 this.say = function (name, say){ vs.set(name, say) .post_to(".box .list"); // 記得清空 $.vmodel.get("md/form").clean(); // 也可以把模板清空 vs.clean(); return vs; } // 使用者回覆了什麼 this.reply = function (name, say, post_to){ vs.set(name, say) .post_to(post_to); } // 將數據放入模板 this.set = function (name, say){ vs.root.find(".name").html(name); vs.root.find(".say").html(say); // 我們加入時間 var NowDate = new Date(); vs.root.find(".current").html(NowDate.toLocaleTimeString()); return vs; } // 放到列表中 this.post_to = function(selector){ //需要先拔除原本的 hidden 屬性才能顯示。 var obj = vs.root.clone() obj.removeAttr('hidden').prependTo(selector); return vs; } // 清空 this.clean = function (){ vs.root.find(".name").html(null); vs.root.find(".say").html(null); vs.root.find(".current").html(null); } }); //訊息模組。這個模組是主動式的。也就是當使用者送出資料以後,並不見得會馬上啟用訊息模組。 $(".message").vmodel("md/message", false, function (){ var vs = this; this.autoload = ['interval']; // 定時更新 this.interval = function (){ //這裡使用 setInterval 作範例,實際上可透過其他效能較好的方式 setInterval(function (){ vs.update(); }, 2000); } // 更新訊息 this.update = function (){ // 應該是由 AJAX 向遠端更新訊息。因為遠端的關係,通常會比較慢才取回資料。 // 我們這邊只假設是本地數據。並延遲觸發來模擬遠端的感覺。 setTimeout(function (){ var NowDate = new Date(); var data = [{ name: "小華", say: "哈囉!", current: NowDate.toLocaleTimeString() }]; var comment = $.vmodel.get("md/comment"); $.each(data, function(index, ele) { comment .set(ele.name, ele.say, ele.current) .post_to(vs.selector + " .liwrap"); }); }, 200); } }); // 全部都定義好了,我們去觸發 box 模組與 message 模組吧 $.vmodel.get("md/box", true); $.vmodel.get("md/message", true); /*******/ // 回覆框模組 $(".box_reply").vmodel("md/box_reply", false, function (){ var vs = this; this.autoload = ['init']; //初始化 this.init = function (){ //將回覆列表與表單,放到 .box_reply 裡面 vs.create_list() .create_form(); } //建立表單 this.create_form = function (){ //讓 "md/form_reply" 初始化 $.vmodel.get("md/form_reply", true); return this; } //建立列表 this.create_list = function (){ //讓 "md/list_reply" 初始化 $.vmodel.get("md/list_reply", true); return this; } // 複製模版,並放到指定的地方 this.post_to = function (selector){ var newobj = vs.root.clone(); //這時候先不要移除 .form_reply 的 hidden,因為我們要等到使用者需要回覆時,才會顯示。 newobj.removeAttr("hidden").appendTo(selector); } }); //回覆表單模組 $(".form_reply").vmodel("md/form_reply", false, function (){ var vs = this; this.autoload = ['init']; //初始化 this.init = function (){ //放到位置 vs.root.appendTo('.box_reply'); } //顯示表單 this.show = function (selector){ $(selector).find(".form_reply").removeAttr('hidden'); } //送出使用者回覆訊息 this.send = function (selector){ var name = $(selector).attr("data-user-name"); var text = $(selector).find(".text").val(); // 等候重整的回覆列表是誰 var who = $(selector).parents(".box_reply").find(".list_reply"); //通常我們可以使用 AJAX 送出,但我們這裡使用本地的模擬延遲 setTimeout(function() { //重新讀取該則回覆列表 $.vmodel.get("md/list_reply").reload(who, function (){ //也許可以做一些事情.... }); // 清空表單 vs.clean(selector); }, 100); } //清空哪個回覆表單 this.clean = function (selector){ $(selector).find(".text").val(null); } }); //回覆列表模組 $(".list_reply").vmodel("md/list_reply", false, function (){ var vs = this; this.autoload = ['init']; // 初始化 this.init = function (){ //放到位置 vs.root.appendTo('.box_reply'); } //哪個回覆列表,需要重新讀取 this.reload = function (selector, callback){ //假設我們模擬透過 AJAX 取得遠端的數據 var data = [{ name: "新人", text: "早安 (其實我是本地產生的數據)" }, { name: "新人2", text: "午安 (其實我也是本地產生的數據)" }]; $.each(data, function(index, info) { //我們要呼叫一開始的模組,因為避免重新設計,討論 .comment 都是使用一款模組。 $.vmodel.get("md/comment").reply(info.name, info.text, selector); }); if (callback) callback(); } }); // 支解後HTML,透過 vmodel 先拼裝起來。 $.vmodel.get("md/box_reply", true); // 送出留言後,添加回覆框。所以在 "md/form" this.submit() 添加送出後的動作 //使用者想要回覆留言,所以添加了事件,在 "md/list" this.when_reply() })
建立關係
要添加新的模組,我們一樣,要先透過 vmodel 渲染彼此的關聯。把每個新模組拼湊起來。
// 支解後HTML,透過 vmodel 先拼裝起來。 $.vmodel.get("md/box_reply", true);
先讓 md/box_reply 觸發初始化吧
// 回覆框模組 $(".box_reply").vmodel("md/box_reply", false, function (){ var vs = this; this.autoload = ['init']; //初始化 this.init = function (){ //將回覆列表與表單,放到 .box_reply 裡面 vs.create_list() .create_form(); } //建立表單 this.create_form = function (){ //讓 "md/form_reply" 初始化 $.vmodel.get("md/form_reply", true); return this; } //建立列表 this.create_list = function (){ //讓 "md/list_reply" 初始化 $.vmodel.get("md/list_reply", true); return this; } // 複製模版,並放到指定的地方 this.post_to = function (selector){ var newobj = vs.root.clone(); //這時候先不要移除 .form_reply 的 hidden,因為我們要等到使用者需要回覆時,才會顯示。 newobj.removeAttr("hidden").appendTo(selector); } });
目前我們只是去呼叫 “md/form_reply” 與 “md/list_reply” 讓它們將HTML擺放到我們希望的位置,也就是 .box_reply 底下。
添加隱藏的回覆框
.box_reply 目前還是在最外層喔,沒有歸屬在誰底下。一直到「使用者點選某人的留言文字,才會出現」。所以我們修改 “md/form” 並添加了 this.submit() 添加送出後的動作
// 表單模組 $(".form").vmodel("md/form", false, function (){ var vs = this; this.autoload = ['init_position', 'submit']; // 初始化會被搬移到 .box 底下 this.init_position = function (){ vs.root.appendTo(".box"); } // 取得輸入的文字 this.user_say = function (){ var val = vs.root.find(".text").val(); return $.trim(val); } // 取得使用者是誰 this.user_name = function (){ return $.trim(vs.root.find(".userdata").attr("data-user-name")); } // 負責放到 .comment this.put = function (user, say, callback){ $.vmodel.get("md/comment").say(user, say); if (callback) callback(); } //送出時... this.submit = function (){ vs.root.on("submit", ".userdata", function (){ var user = vs.user_name(); var say = vs.user_say(); vs.put(user, say, function (){ //發送後可以做一些事情... //將回覆框放進來 var where = $(".list .comment").first(); $.vmodel.get("md/box_reply").post_to(where) }); return false; }) } // 清空 this.clean = function (){ vs.root.find(".text").val(null); } });
沒錯,我們是透過自訂的回呼含式,讓使用者送出討論表單 (.form) 後,會把最外層的回覆框模版,整個複製到留言的最後位置。
//送出時... this.submit = function (){ vs.root.on("submit", ".userdata", function (){ var user = vs.user_name(); var say = vs.user_say(); vs.put(user, say, function (){ //發送後可以做一些事情... //將回覆框放進來 var where = $(".list .comment").first(); $.vmodel.get("md/box_reply").post_to(where) }); return false; }) }
我們看看呼叫 md/box_reply 做放置的部分
// 複製模版,並放到指定的地方 this.post_to = function (selector){ var newobj = vs.root.clone(); //這時候先不要移除 .form_reply 的 hidden,因為我們要等到使用者需要回覆時,才會顯示。 newobj.removeAttr("hidden").appendTo(selector); }
這樣就完成了。我們在最上方打上文字、按下送出,會把文字更新到下方的列表。這時候下方列表,「會加入被隱藏的回覆框」。
當使用者想要針對某則留言做回覆
使用者點了留言文字,這件事情我們要修改在 “md/list” 並添加新的 this.when_reply() ,我們會去呼叫模組 “md/form_reply” 做顯示的動作。
// 當使用者想要回覆該則留言 this.when_reply = function() { vs.root.on("click", ".comment .say", function (){ //顯示該則的回覆表單 var who = $(this).parents(".comment"); $.vmodel.get("md/form_reply").show(who); }); }
注意!為什麼事件要綁定在 “md/list” 模組,而不是在 “md/form_reply” 呢?這是一個經驗上的複雜問題,但歸類到的經驗結果很簡單,因為比較好做。將事件綁定在 “最少動態添加的位置” 是比較適合的,這樣可以大幅降低手寫動態綁定的事情。當然,要把所有事件都綁在同一個HTML標籤當然也可以,但是我們要盡可能分散。每個根元素盡量分開,並讓不同的概念綁定在上面,才會知道每個根元素,帶有各有不同的意義。再來,性能會比較好。下面是說明
.box .form .list <---- 綁在這裡,因為變動次少,不用每次都思考什麼時候還要添加綁定 .comment <-- 這裡常常在添加 .name .say .current .box_reply <-- 綁在這裡,在模版沒問題,但因為上方的 .comment 時常添加,所以連帶這裡也會是新的元素,所以就要重新綁定 .list_reply .comment .name .say .current ~ .form_reply .text .submit_reply ~
現在,使用者回覆表單已經顯示出來了,使用者可以在上面打字,打完以後送出的動作,也會在 “md/list” 這邊
//當送出回覆表單。要綁定在這裡,而不是綁定在 md/comment ,是因為 .list 是不會變動的。 this.when_send_reply = function (){ vs.root.on("submit", ".form_reply", function (){ $.vmodel.get("md/form_reply").send(this); return false; }); }
送出的實際動作我們交給 “md/form_reply” .send() 去執行。並傳入,現在哪張回覆表單要送出。這很重要喔。我們看一下整體的 “md/form_reply” 模組。
//回覆表單模組 $(".form_reply").vmodel("md/form_reply", false, function (){ var vs = this; this.autoload = ['init']; //初始化 this.init = function (){ //放到位置 vs.root.appendTo('.box_reply'); } //顯示表單 this.show = function (selector){ $(selector).find(".form_reply").removeAttr('hidden'); } //送出使用者回覆訊息 this.send = function (selector){ var name = $(selector).attr("data-user-name"); var text = $(selector).find(".text").val(); // 等候重整的回覆列表是誰 var who = $(selector).parents(".box_reply").find(".list_reply"); //通常我們可以使用 AJAX 送出,但我們這裡使用本地的模擬延遲 setTimeout(function() { //重新讀取該則回覆列表 $.vmodel.get("md/list_reply").reload(who, function (){ }); // 清空表單 vs.clean(selector); }, 100); } //清空哪個回覆表單 this.clean = function (selector){ $(selector).find(".text").val(null); } });
我們看送出的地方,
//送出使用者回覆訊息 this.send = function (selector){ var name = $(selector).attr("data-user-name"); var text = $(selector).find(".text").val(); // 等候重整的回覆列表是誰 var who = $(selector).parents(".box_reply").find(".list_reply"); //通常我們可以使用 AJAX 送出,但我們這裡使用本地的模擬延遲 setTimeout(function() { //重新讀取該則回覆列表 $.vmodel.get("md/list_reply").reload(who, function (){ //也許可以做一些事情.... }); // 清空表單 vs.clean(selector); }, 100);
我們送出回覆文字,會需要馬上更新該則訊息的「回覆列表」,所以我們要知道等下是「哪篇訊息的回覆列表」需要被更新。所以更新回覆列表,我們看 “md/list_reply” 的 reload()
//哪個回覆列表,需要重新讀取 this.reload = function (selector, callback){ //假設我們模擬透過 AJAX 取得遠端的數據 var data = [{ name: "新人", text: "早安 (其實我是本地產生的數據)" }, { name: "新人2", text: "午安 (其實我也是本地產生的數據)" }]; $.each(data, function(index, info) { //我們要呼叫一開始的模組,因為避免重新設計,討論 .comment 都是使用一款模組。 $.vmodel.get("md/comment").reply(info.name, info.text, selector); }); if (callback) callback(); }
因為該篇留言,可能有很多人回覆了,所以我們透過AJAX取得資料,批次將它顯示,也就是透過 “md/comment” .reply() 將訊息放入。
“md/comment” 在上篇例子已經存在,他是負責處理每則討論訊息的模組。我們這裡擴充了 .reply() 負責處理回覆的訊息。
//討論模組 $(".comment").vmodel("md/comment", false, function (){ var vs = this; // 使用者說了什麼 this.say = function (name, say){ vs.set(name, say) .post_to(".box .list"); // 記得清空 $.vmodel.get("md/form").clean(); // 也可以把模板清空 vs.clean(); return vs; } // 使用者回覆了什麼 this.reply = function (name, say, post_to){ vs.set(name, say) .post_to(post_to); } // 將數據放入模板 this.set = function (name, say){ vs.root.find(".name").html(name); vs.root.find(".say").html(say); // 我們加入時間 var NowDate = new Date(); vs.root.find(".current").html(NowDate.toLocaleTimeString()); return vs; } // 放到列表中 this.post_to = function(selector){ //需要先拔除原本的 hidden 屬性才能顯示。 var obj = vs.root.clone() obj.removeAttr('hidden').prependTo(selector); return vs; } // 清空 this.clean = function (){ vs.root.find(".name").html(null); vs.root.find(".say").html(null); vs.root.find(".current").html(null); } });
在這裡我們只需要將數據放入模版、再放到指定地點,這兩件事情而已。
// 使用者回覆了什麼 this.reply = function (name, say, post_to){ vs.set(name, say) .post_to(post_to); }
好了,這次擴充回覆留言功能完成了喔!是不是讓你的思維更加清晰了呢?每個模組各司其職,每個方法只做一件小單位的處理,是物件導向的概念。如果本身就使用物件導向開發,一定很快就能將這個概念,透過 vmodel 建構出你的前端介面囉。
Comments