先說點閒話,熟悉Angular的猿們會喜歡這個插件的。
00.本末倒置
我不得不承認我是一個喜歡本末倒置的人,學生時代就喜歡先把晚交的作業先做,留著馬上就要交的作業不做,然後慢悠悠做完不重要的作業,臥槽, XX作業馬上要交了,趕緊補補。如今做這個項目,因為沒找到合適的多選下拉Web插件,又不想用html自帶的醜陋的,自己花了一整天時間做了一個。或許這樣佔用的主要功能開發的時間,開發起來會更有緊迫感。感覺自己是個抖M自虐傾向,並且伴隨css和代碼縮排強迫症的程式猿猴。
01.畫蛇添足
Angular強大的控制器似乎已經可以滿足大部分UI上的需求了,但是NodeJS應用程式往往會使用ejs,jade這樣的模板引擎來動態產生html頁面,那麼問題來了,當我想把後台傳給express中res.render()的參數直接顯示到介面而且綁定到對應的ng-model怎麼辦?
解決方法1,不要什麼事一次來,Angular的Controller發個post請求再拿資料不就行了
解決方法2,先用模板暫存在html上,再讓Controller依照頁面上的資料來初始化$scope的值
解決方法3,鄙對Angular和EJS才疏學淺,誰有好辦法教我唄
例如現在要做一個選擇下拉框,選項在後台,我不想單獨發post拿,也不想放在頁面上,Controller單獨寫邏輯處理,而Angular社群有個ui-select插件,看起來數據是從$scope取的,並不是直接拿的標籤的數據,當時我就火了,不就一個下拉框,自己做唄。
10.樂觀的程序猿
思路很明確,定義一個Angular directive -> 把選項值拿出來 -> 各種事件加加加 -> scope資料綁定 -> 完結撒花
我估計的時間是半天,然而實際花了多久只能呵呵了,css強迫症,Angular理解不深(所以很多html操作還是在用jQuery),事件考慮不全導致了最終花了超過兩倍的時間做完,
不廢話了,簡單實用,既可以即時綁定ng-model $scope.xxx,也可以直接調jQuery的$("標籤的id").val()也能拿到值,
git傳送門duang:https://git.oschina.net/code2life/easy-select.git
demo傳送門duang~duang:http://ydxxwb.sinaapp.com/easy-select-demo/ (代碼不是最新,有兩個fix的bug還沒部署上去)
11.放碼
1.使用方法: 引入庫檔案Bootstrap,Angular1.x,引入style.css檔案(可以修改css自訂自己想要的樣式),easy-select.js,定義Angular的Controller,依賴easySelect模組,像這樣↓
angular.module('dataDisplay', ['easySelect']).controller('selectController', ['$scope', '$http',function ($scope, $http) { // your code }]);
然後參考demo範例的規範定義選擇框就行啦,是不是很有html原生select標籤的親切感
2.源碼解釋:dom操作和事件都是用jQuery實現的,每一步都有簡略的註釋,實現雙向綁定的關鍵在於取得標籤上定義的ng-model,然後在事件中設定scope[ ng-model]的值,
並且呼叫$digest()循環來讓Angular根據ng-model更新DOM,$digest是Angular實現雙向綁定的核心之一,原理是將變化的scope值同步到所有需要更新的地方,實現暫時還不大明白,有空單獨研究一下這些Angular裡面$,$$開頭的東西。
3.自適應與css,Bootstrap就是自適應的,css可以自己客製不同的風格,style.css都有相關註解
easy-select.js
var comDirective = angular.module('easySelect', []); comDirective.directive("easySelect", function () { return { link: function (scope, element, attrs) { var ngModel = $(element).attr("ng-model"); if(!ngModel || ngModel.length == 0) { ngModel = "defaultSelectModel"; } var status = false; //toggle boolean var valueMap = ""; var options = $(element).children(); $(element).attr("style", "padding:0"); //hide original options $.each(options, function (opt) { $(options[opt]).attr("style", "display:none"); }); //build ul var html = "<div id='" + attrs.id + "-root' style='width:100%;position: relative;left:-1px'>" + "<p id='display-"+attrs.id + "' style='padding:6px 12px "+ ((attrs.multiple != undefined)?"4px":"7px")+ " 12px;margin:0;border:none;width:95%;margin-left:2px;background-color: transparent'>" + "<span style='display: inline-block;padding-bottom: 3px'> </span></p>" + //this is a dummy span "<ul id='" + attrs.id + "-container' class='list-group easy-select-container' style='display:none'>"; //options' container if(attrs.multiple != undefined) { $.each(options, function (opt) { html += "<li value='"+ $(options[opt]).val() +"' class='my-li-container list-group-item option-"+ attrs.id+ "'><div style='width:100%;display:inline-block'>" + $(options[opt]).html() + "</div><span value='"+ $(options[opt]).val() +"' class='my-li-option glyphicon glyphicon-ok'></span></li>"; }); } else { $.each(options, function (opt) { if($(options[opt]).attr("default") != undefined) { scope[ngModel] = $(options[opt]).val(); valueMap = $(options[opt]).html(); html += "<li value='"+ $(options[opt]).val() +"' class='my-li-container list-group-item option-"+ attrs.id+ "'>" + $(options[opt]).html() + "</li>"; } else { html += "<li value='"+ $(options[opt]).val() +"' class='my-li-container list-group-item option-"+ attrs.id+ "'>" + $(options[opt]).html() + "</li>"; } }); } //if multiple, add button if (attrs.multiple != undefined) { html += "<li class='list-group-item ' for='ensure-li'><button class='btn btn-default'" + " for='ensure-btn' style='padding: 2px' > 确定 </button></li>"; } //render ui html += "</ul></div>"; $(element).append(html); $(".my-li-option").each(function(){ $(this).fadeOut(0); }); if(attrs.multiple == undefined) $($("#display-"+attrs.id).children()[0]).html(valueMap); //adjust width $("#" + attrs.id + "-root").width($("#" + attrs.id + "-root").width() + 2); //mouse leave event $(element).mouseleave(function(){ $(".my-li-container").each(function(){ $(this).attr("style",""); }); if(status) { $("#" + attrs.id + "-container").attr("style", "display:none"); status = !status; } }); //multiple select seems complex if (attrs.multiple != undefined) { //click event $(element).click(function (e) { //if click on tags, remove it if($(e.target).attr("for") == "option-tag") { // change val and digest change item in angular scope[ngModel] = $(element).val().replace($(e.target).attr("value"),"").replace(/;+/,";").replace(/^;/,""); $(element).val(scope[ngModel]); scope.$digest(); $(e.target).remove(); $(".my-li-option").each(function(){ if($(this).attr("value") == $(e.target).attr("value")) { $(this).css("opacity","0.01"); } }); } else if($(this).attr("for") != 'ensure-li') { //toggle ul $("#" + attrs.id + "-container").attr("style", status ? "display:none" : ""); status = !status; } }); $(".option-"+attrs.id).each(function(){ $(this).on('click',function(){ var selectValue = $(element).val(); var currentValue = $(this).attr("value"); var selected = false; //if option is selected ,remove it var temp = selectValue.split(";"); $.each(temp,function(obj){ if(temp[obj].indexOf(currentValue) != -1) { selected = true; } }) if(selected) { $($(this).children()[1]).fadeTo(300,0.01); scope[ngModel] = $(element).val().replace(currentValue,"").replace(/;{2}/,";").replace(/^;/,""); $(element).val(scope[ngModel]); scope.$digest(); $("#display-"+attrs.id + " span").each(function(){ if($(this).attr("value") == currentValue) { $(this).remove(); } }); } else { //add option to val() and ui $($(this).children()[1]).fadeTo(300,1); scope[ngModel] = ($(element).val()+";"+currentValue).replace(/;{2}/,";").replace(/^;/,""); $(element).val(scope[ngModel]); scope.$digest(); $("#display-"+attrs.id).append( "<span for='option-tag' value='"+ $(this).attr("value") +"' class='p-option-tag'>" +$(this).children()[0].innerHTML+ "</span>"); } status = !status; // prevent bubble }); //control background $(this).mouseenter(function(){ $(".my-li-container").each(function(){ $(this).attr("style",""); }); $(this).attr("style","background-color:#eee"); }); }); } else { $(".option-"+attrs.id).each(function(){ $(this).mouseenter(function(){ $(".my-li-container").each(function(){ $(this).attr("style",""); }); $(this).attr("style","background-color:#eee"); }); }); //single select ,just add value and remove ul $(element).click(function () { $("#" + attrs.id + "-container").attr("style", status ? "display:none" : ""); status = !status; }); $(".option-"+attrs.id).each(function(){ $(this).on('click',function(){ scope[ngModel] = $(this).attr("value"); $(element).val(scope[ngModel]); scope.$digest(); console.log(ngModel); console.log(element.val()); $($("#display-"+attrs.id).children()[0]).html($(this).html()); }); }); } } } });
100.如果看到了這裡,表示對這個小東西有興趣,git上一起完善吧,自訂選項模板,選項分組這兩個功能還沒有實現。少年,加入開源的大軍吧。
以上所述是小編給大家分享的自訂Angular指令與jQuery實現的Bootstrap風格資料雙向綁定的單選與多選下拉框,希望大家喜歡。