jQuery – vmodel.js 討論列表範例教學(3) 如何改版?

這次的例子簡單多了,旨在簡化改版這件事情的介紹。我們模擬當版面設計僅需要小幅度修改時,如何應對。

先去看線上範例。

大家知道在 jQuery 的推廣概念中,將 JavsScript、HTML、CSS 支解分離是能夠有效的降低彼此的依附關聯,對於編碼來說會非常乾淨,也容易理解、容易修改。針對之前的例子,我們試試看如何透過 vmodel 做修改。

假設需求是要添加刪除按鈕。

舊款

擷取

新款

擷取

當滑鼠移入時,會出現刪除按鈕,這個動作不是太過花俏,所以可以從 CSS 直接修改。而 HTML 的部分,也只需要修改 .comment 的編碼,加入新的元素即可。目前為止不會動到任何的 jQ。

修改結構

<!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="index3.js"></script><!-- 範例編碼 -->
    <link rel="stylesheet" href="index3.css"><!-- 範例樣式 -->
</head>
<body>
    

    <!-- 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><span class="del">X</span>
    </div>

    <!-- 提示訊息顯示 -->
    <div class="message">
        <div class="title">Message</div>
        <div class="liwrap"></div>
    </div>

    <!-- 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>

其中這段後面的 .del 是新添加的元素,就是代表那顆刪除的叉叉。

<!-- comment 預先隱藏起來 -->
<div class="comment" hidden>
    <span class="name"></span> <span class="say"></span> - <span class="current"></span><span class="del">X</span>
</div>

 

開始修改邏輯部分

當我們把HTML編碼調整到你要的定位後,設定新的 CSS ,接著才來動作 vmodel 的部分。完整程式碼如下

$(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', 'delete'];

        // 這裡只負責推送到 .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;
            });
        }

        // 刪除comment
        this.delete = function (){
            vs.root.on("click", ".del", function (){
                // 呼叫模組 md/comment 刪除元素
                $.vmodel.get("md/comment").remove_element(this);
            });
        }

    });


    // 整體框的主要模組
    $(".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);
        }

        //刪除該筆討論
        this.remove_element = function (selector){
            //透過CSS動畫模擬刪除動作
            var $this = $(selector);
            $this.parent(".comment").css({
                transition: "all 0.4s",
                transform: "translateX(180px)",
                opacity: 0
            });
            //簡易的當CSS動畫結束時,刪除元素
            setTimeout(function (){
                $this.parent(".comment").remove();
            }, 400);
        }

        //刪除誰底下的 comment 刪除鈕
        this.remove_delete_button = function (selector_find){
            var button = vs.root.find(".del");
            if (button.length == 0) return true;
            $(selector_find).find(vs.selector + " .del").remove();
        }
    });

    //訊息模組。這個模組是主動式的。也就是當使用者送出資料以後,並不見得會馬上啟用訊息模組。
    $(".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");

                    // 添加刪除討論的刪除元素, 因為不需要出現。
                    var button = vs.root.find(".del");
                    if (button.length == 0) return true;

                    // 呼叫模組的刪除按鈕指令
                    $.vmodel.get("md/comment").remove_delete_button(vs.selector);
                });

            }, 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/list" 添加函式 delete(), 綁定點擊刪除鈕的動作

    // 因為 .message 底下的 .comment 使用同一個模組,但是不應該出現刪除按鈕。
    // 所以修改 "md/message" 的 update(); 
})

首先,我們要做 「點擊後觸發刪除該則討論」,所以 “md/list” 添加函式 delete()

// 刪除comment
this.delete = function (){
    vs.root.on("click", ".del", function (){
        // 呼叫模組 md/comment 刪除元素
        $.vmodel.get("md/comment").remove_element(this);
    });
}

一樣記得刪除這件事情,是由 “md/list” 綁定喔!至於為什麼就參考 上一篇 的說明。當事件綁定後,會去呼叫 “md/comment” 的 remove_element()

//刪除該筆討論
this.remove_element = function (selector){
    //透過CSS動畫模擬刪除動作
    var $this = $(selector);
    $this.parent(".comment").css({
        transition: "all 0.4s",
        transform: "translateX(180px)",
        opacity: 0
    });
    //簡易的當CSS動畫結束時,刪除元素
    setTimeout(function (){
        $this.parent(".comment").remove();
    }, 400);
}

這邊要記得使用 parent() 而不是 parents() ,不然會刪除到最頂端的選擇器。透過這支函式,我們就做到刪除的動作了。

那麼接下來,因為 .message 也使用了相同的 comment 模組,只是 .message 屬於提示訊息,我們並不希望他有刪除的按鈕,這該怎麼辦呢?沒關係,我們來看看接著要修改 “md/message” 的 update();

$.each(data, function(index, ele) {
    comment
        .set(ele.name, ele.say, ele.current)
        .post_to(vs.selector + " .liwrap");

    // 添加刪除討論的刪除元素, 因為不需要出現。
    var button = vs.root.find(".del");
    if (button.length == 0) return true;

    // 呼叫模組的刪除按鈕指令
    $.vmodel.get("md/comment").remove_delete_button(vs.selector);
});

這邊我們只要再添加一些判斷,並透過模組 “md/comment” 來呼叫 remove_delete_button() ,如此一來就能夠移除 .message 底下 .comment 的 .del 喔!

//刪除誰底下的 comment 刪除鈕
this.remove_delete_button = function (selector_find){
    var button = vs.root.find(".del");
    if (button.length == 0) return true;
    $(selector_find).find(vs.selector + " .del").remove();
}

 

結語

在製作使用者介面的時候,基本上還是把 HTML 的標籤,放置在 HTML 的位置,CSS 的部分寫在 CSS 的位置,而不是放在任何的 JavaScript 或  jQuery 的部分。jQuery.vmodel() 主要沿用這樣的概念設計,當我們需要換版面或是添加小零件時,才不會發生「我這句 HTML 應該要去 HTML 的位置找呢,還是要去看 jQuery 的部分?」統一集中處理,是非常必要的習慣。像我們這邊因為要添加刪除按鈕,很自然的,會先去處理 HTML + CSS ,之後才會去處理 jQuery.vmodel() 的邏輯。希望透過這些範例教學,可以讓你感受到使用 vmodel() 設計的複雜介面時的優點,歡迎大家下載來玩玩

 

完整教學範例

 

發表迴響