顯示具有 AngularJS 標籤的文章。 顯示所有文章
顯示具有 AngularJS 標籤的文章。 顯示所有文章

2025年10月29日 星期三

AngularJS - Component 使用紀錄

紀錄一下 AngularJs 的 Component 使用方法

HTML :
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>

<div ng-app="app" ng-controller="controller as ctrl">
  <attach-uploader some-variable="ctrl.parentvariable"
                   some-event="ctrl.parentFunction(text1, text2)">
  </attach-uploader>
</div>
JS :
var app = angular.module("app", []);
app.controller("controller", [function() {
   var self = this;
   this.parentvariable = "xxx";
   this.parentFunction = function(someText1, someText2) {
      console.log(someText1);
      console.log(someText2);
   }
}]);
app.component("attachUploader", {
  template: `<div>
	        <div ng-repeat="uploader in $ctrl.uploaderList track by $index">
                   <input type="file"
                          ng-attr-name="attachFile_{{$index}}"
                          file-validator="fileType:'png';"
                          /> 
                   <button ng-click="$ctrl.addUploader()">+</button>
                </div>
                <button ng-click="$ctrl.onXxxEvent('aaa', 'bbb')">Test button</button>
	      </div>`,
  bindings: {
     someVariable: "=",
     someEvent: "&" // function 的綁定
  },
  //controllerAs: "ctrl", //預設是  $ctrl
  controller: ["otherServiece", function (otherService) {
    var self = this

    self.maxUploader = 5
    self.uploaderList = [{}]
    
    console.log('Test1: ' + self.someVariable); //會是 undefined
    self.$onInit = function() {
       console.log('Test1: ' + self.someVariable); //會是 xxx,要在 $onInit() 裡才會得到值
	}
    
    self.$onChanges = function() {
       //component 的輸入有變化時會呼叫
    }
    
    self.$doCheck = function() {
       //component 每一次的 Digest Circle 都會呼叫,適合用在偵測 AngularJs 不會去偵測 (就是不會觸發 $onChanges) 的物件上的變化 
    }
    
    self.onXxxEvent = function(text1, text2) {
       self.someEvent({text1: text1, text2: text2}); //傳遞含有參數資訊的 Map 給 parent 的 function
    }

    self.addUploader = function () {
      if (self.uploaderList.length >= self.maxUploader) {
        return
      }
      self.uploaderList.push({})
    }
  }],
});

2024年2月29日 星期四

AngularJS - 自製分頁 Filter

紀錄一下 AngularJS 自製分頁 Filter 的方法

HTML:

<div ng-app="app" ng-controller="controller as ctrl">
  <div ng-repeat="item in ctrl.itmeList | paging:ctrl.page:ctrl.pageSize">
    {{item}}
  </div>
  
  <div>  
    Page: <input type="number" ng-model="ctrl.page"/>
  </div>
  <div>  
    Page size: <input type="number" ng-model="ctrl.pageSize"/> Page
  </div>
</div>

Javascript:

var app = angular.module("app", []);
app.controller("controller", [function() {
	var self = this;
  self.itmeList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  self.page = 1;
  self.pageSize = 3;
}]);

app.filter('paging', function() {
	    return function(input, page, pageSize) {
	        return input.slice(page * pageSize - pageSize, page * pageSize);
	    }
});

JsFiddle 範例:

參考資料:

  1. Filters

2022年9月18日 星期日

在同一個頁面上建立多個 AngularJs 的 module 及 controller - 使用 angular.bootstrap()

ng-app 是很常見的 AngularJs 頁面 module 的設定,
但這樣的方式會有局限性,
因為 AngularJs 只允許一個頁面使用一個 ng-app,
如果有多個 ng-app 則會錯誤或不正常運作。

不過這並不代表 AngularJs 不能在一個頁面上設定多個 module,
AngularJs 可以使用
angular.bootstrap(element, moduleNameArray)
來在一個頁面上設定多個 module,
其中參數 element 是 module 所在的 DOM Object,
而 moduleNameArray 則是個 array<String>,
內部放著要設定到頁面上的 module 名稱。

以下是一個範例,
頁面上有三個要被設定 module 的 DOM Object,
分別是 id="module_1" 、 id="module_2" 和 id="module_3",
然後我們會先設定兩個  AngularJs module,module 名為 "module_A" 和 "module_B",
各 module 裡面各有一個名為 "controller" 的 controller,
我們要把 module_A 設定到 #module_1 和 #module_2 上,
及把 module_A 和 module_B 一起設定到 #module_3 上。

html:
<div id="module_1">
  module_1:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>

<div id="module_2">
  module_2:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
  
   <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>

<div id="module_3">
  module_3:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
  
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>
Javascript:
angular.module("module_A", [])
.controller("controller", [function(){
	var self = this;
  self.text = "A";
}])

angular.module("module_B", [])
.controller("controller", [function(){
	var self = this;
  self.text = "B";
}]);

angular.bootstrap(document.getElementById("module_1"), ["module_A"]);
angular.bootstrap(document.getElementById("module_2"), ["module_A"]);
angular.bootstrap(document.getElementById("module_3"), ["module_A", "module_B"]);
以下是 Jsfiddle 上的成品:
說明:
首先可以先注意到,在 html 中的
id="module_2" 中
故意擺放了名稱一樣的 controller:
ng-controller="controller as ctrl"
這裡是要展示一個特性,就是即使同一個 module 下有相同 controller 名稱的 controller,
也就是它們都使用了在 javascript 中同一個 module 之下的同樣名為 "controller" 的 controller 設定,
它們的 scope 仍然是不同的,方便想像可以把它們看做是同一個 function 所 new 出來的不同  Class 實體 (Class Instance),
所以它們設定的 ng-model 是各自獨立的,並不會互相連動,
可以從 Jsfiddle 上的成品範例實際使用觀察看看。

再來在 html 中的
id="module_1" 和 id="module_2" 雖然都用了在 javascript 中相同的名為 "module_A" 的 module 設定,
不過它們仍然算是互相獨立的不同 module 個體,跟之前同個 module 下的同名 controller 說明很像,
所以它們彼此的同名 controller 的 ng-model 也是不會互相連動的。

最後是在 html 中的
id="module_3",它被設定了 module_A 和 module_B,
所以它可以得到 module_A 之下及 module_B 之下的 controller 設定,
有點像是一個載入了 module_A 和 module_B 的 module_C 一樣,
可以方便想像成如下:
angular.module("module_C", ["module_A", "module_B"]),
然後當然的,跟上面說明的情況一樣,
id="module_3" 上設定的 module 跟 controller 一樣是獨立於 id="module_1" 和 id="module_2",
事實上,id="module_1"、id="module_2" 和 id="module_3" 上的 module 跟 controller 彼此都是互相獨立無關的,
唯一的共同點就只有都有使用到了 module_A 的設定而已。

最後的結果是範例中四個 <input> 的值彼此都不是互相連動的。
這在如果你想要把相同程式邏輯的 module 做成元件,
並放到頁面上的各處,又不想它們彼此共用 scope (即共用 ng-model 去連動改變 scope 中的變數值) 時,
會是一個可利用的不錯的特性。

是 Luis 作者寫的一篇關於 ng-app 限制的文章,
也一樣用到了 angular.bootstrap 去解決 ng-app 限制的問題,
並在 Github 上實作了一個方便來用 DOM 屬性設定 AngularJs module 的 module 工具,
源碼部份也是使用了一樣的觀念,
找出要設定 module 的 DOM,從屬性中讀到要被設定的 module 名稱後進行 module 的設定,
十分地具有參考的價值。

2021年1月12日 星期二

實作 angularJs 自訂驗證 select option list - 檢查 ng-model 的值有無在 option list 中

 AngularJs 原生的 select option  的 required 驗證,

只能對 ng-model 的值為 undefined , "", null , NaN來做 required error。

參考:

ngRequired

ngModel.NgModelController 的 $isEmpty

但如果 ng-model 不是 undefined , "", null , NaN ,且其值不在 select option 中,

此時希望能檢查出驗證錯誤的話 (validation) ,

就需要自己實作。


例如可能的情境為:

如果 option list 裡都是不為 0 的數字,當 ng-model 是數字 0 時 (可能是程式常態 assign 賦值的),此時 required (或 ng-required) 就無法驗證出 required 的 validation error。


以下為我寫出來的簡單實作,

建立了一個名為 selectOptionCheckValidator 的 directive 來進行驗證:



html : 

<div ng-app="app" ng-controller="controller as ctrl">
  <form name="form">
    <label><input type="radio" name="optionType" ng-value="1" ng-model="ctrl.optionType" /> Set option list 1</label>
    <label><input type="radio" name="optionType" ng-value="2" ng-model="ctrl.optionType" /> Set option list 2</label>

    <select name="mySelector" ng-model="ctrl.selectedVal" ng-options="selectOption.optionValue.id as selectOption.title for selectOption in ctrl.selectOptionList" select-option-check-validator ng-required="true">
    </select>
    
    <button ng-click="ctrl.selectedVal = 0">
      Set selectedVal to 0
    </button>

    <div>
      ctrl.selectedVal : {{ctrl.selectedVal}}
    </div>

    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.selectedValueInOptionList ? "invalid" : "valid"}}
    </div>
    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.required ? "invalid" : "valid"}}
    </div>

  </form>
</div>



Javascript :
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
    var self = this;
    self.optionType = 1;
    self.selectedVal = 0;

    self.optionList1 = [{
      optionValue: {
        id: 11
      },
      title: "id 11"
    }, {
      optionValue: {
        id: 12
      },
      title: "id 12"
    }];

    self.optionList2 = [{
      optionValue: {
        id: 21
      },
      title: "id 21"
    }, {
      optionValue: {
        id: 22
      },
      title: "id 22"
    }];

    self.selectOptionList = [];

    $scope.$watch(angular.bind(this, function() {
      return this.optionType;
    }), function(newVal, oldVal) {
      //if value of selectedValue can not be found in self.selectOptionList,
      // selectedValue will be assigned to null.
      if (newVal == 1) {
        self.selectOptionList = self.optionList1;
      } else if (newVal == 2) {
        self.selectOptionList = self.optionList2;
        self.selectedVal = 21;
      }
    });
  }]).directive('selectOptionCheckValidator', ["$parse", function($parse) {
    return {
      require: 'ngModel',
      link: function(scope, element, attrs, ngModel) {

        var regex = /(.+)\sas\s(.+)\sfor\s(.+)\sin\s(.+)/;
        var matchString = regex.exec(attrs.ngOptions);
        var collectionExpression = matchString[4]; // "ctrl.selectOptionList"
        var arryItemVarableName = matchString[3]; // "selectOption"
        var valueExpression = matchString[1]; // "selectOption.optionValue.id"

        scope.$watch(collectionExpression, function(newValue, oldValue) {
          checkIsSelectedValueInOptionList();
        });

        element.on("change", function() {
          scope.$apply(function() {
            checkIsSelectedValueInOptionList();
          });
        });

        function checkIsSelectedValueInOptionList() {
          var isSelectedValueInOptionList = false;
          var selectedValue = $parse(attrs.ngModel)(scope);
          var optionList = $parse(collectionExpression)(scope);

          if (optionList) {
            isSelectedValueInOptionList = optionList.some(function(option) {
            
              // create a custom scope object to render optionValue from valueExpression
              var optionTempObject = new function(){
              	this[arryItemVarableName] = option;
              };
              
              var optionValue = $parse(valueExpression)(optionTempObject);
              
              return optionValue == selectedValue;
            });
            
            ngModel.$setValidity("selectedValueInOptionList", isSelectedValueInOptionList);
          }          
        }
      }
    };
  }]);



在這個例子裡,設置了兩個 option list 作為 <select> 的 option 選項,
當按下例子中的兩個 input type="radio" 時會改變 optionType ,
而 optionType 被改變時,會根據所勾選的值來重新設置不同的 option 選項給 <select>。

在 html <select> 的下方,我把 selectedVal 和 <select> 的 validation error 印出來以利觀察。

可以看到的是,
一開始 selectedVal 被設定成 0,但兩個 option 選項中都並沒有 0 這個值存在,
如果我們希望當 selectedVal 不在 option 選項中時能夠被驗證出來產生 validation error,
就必須自己實作 directive 來達成,即在這個例子中的 selectOptionCheckValidator directive。

AngularJs 原生的 required (或 ng-required) 在 selectedVal = 0 時,
會無法產生 validation error,因為它的實作是在當 selecctedVal = undefined, null, "", NaN 時才會產生 validation required error.
-------------------------------------------------------------------------------------------------------------------
另一個可以注意到的是,
我在這裡對按下第二個 input type="radio" 時,對 selectedVal 賦與 option list 2 中的值,
而按下第一個 input type="radio" 時沒有賦值。
此時可以觀察到當在一個 AngularJs Render 生命週期中,
如果 option list 被改變了,且 selectedValue 在 option list 中找不到的話,
selectedValue 就會被重新設定成 null。

Note

此例中用來解析 ng-option 字串的正規表達式較為簡易,

如果想知道完整的正規表達式,也就是可以解析 AngularJs ng-option 所有可能字串的正規達式,

可以參考官方源碼,其中可以看到正規表達式為:

^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$

圖例



2021年1月11日 星期一

實作能偵測 textarea 內容並自動調整高度的 angularjs directive

實作了能偵測 textarea 內容並自動調整高度的 angularjs directive。

當 <textarea> 裡的文字變多需要高多行的 textarea 時,即需要更高的 textrea 時,

可以自動的調高高度,

而當文字變少時也能自動減少高度。

Note:

因為有使用到一些 jQuery 的 function,例如 innerHeight,所有需要先 include jQuery

直接上程式碼及範例:

html:
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>

This &lt;textarea&gt; will change its height to contain content.
<div ng-app="app" ng-controller="controller as ctrl">
  <textarea textarea-auto-resizer></textarea>
</div>

javascript:
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
  	var self = this;
    self.text = "long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words"
  }])
  .directive('textareaAutoResizer', ["$sce", function($sce) {
    return {
      link: function(scope, element, attrs, ngModel) {
        autoResizeTextArea();

        scope.$watch(attrs.ngModel, function(newValue, oldValue) {
        	autoResizeTextArea();
        });
        
        element.on("input", autoResizeTextArea);

        function autoResizeTextArea(event) {
          var dom = angular.element(element);

          dom.css({
            height: ""
          });
          if (dom.prop("scrollHeight") > dom.innerHeight()) {
            //set dom height(without padding part) to scrollHeight - padding part
            dom.height(dom.prop("scrollHeight") - (dom.innerHeight() - dom.height()));
          }
        }
      }
    };
  }]);


參考:

  1. Creating a Directive that Adds Event Listeners

2019年12月29日 星期日

AngularJs - Service


在使用 Angularjs 的 directive 時,
有時我們會想要在外層 (即使用 directive 的 controller 或其他 directive) 去呼叫 directive 裡面的 function,
去改變一些 directive 的內部狀態,
這時使用 service 就是一個比較好的做好,
service 可以作為 directive 及使用 directive 的元件 (例如外部 controller 或其他 directive)
的溝通橋梁,
利用把 directive 的 scope 或 內部 controller 綁定設定到 service 中,
我們即可在外部將 service 注入,並使用 service 來控制 directive。

下面給出一個範例 : 可以看到,<my-form-directive> 是我們實作的一個 directive,
裡面有一個名為 "pristineForm" 的 function ,
其可以設定 $scope 裡面的 myForm (為在 directive template 中 name="myForm" 的 <form>)
成初始狀態 ( 使用 formController.$setPristine() )。

在 <my-form-directive> 外,有一個 <div> 被設定了  ng-click="ctrl.pristineForm() ,
會去呼叫外層 controller 的 pristineForm(),
而 pristineForm() 會利用引入的 directiveHandler 這個 service ,
去呼叫 myFormDirective 這個 directive 內部的 pristineForm()。

下面是詳細的程式碼:

Javascript :
angular.module("app", [])
  .controller("controller", ["directiveHandler", function(directiveHandler) {
    var self = this;
    self.pristineForm = function(directiveName) {
     var directiveName = directiveName ? directiveName : 'myFormDirective';
      directiveHandler.getDirective(directiveName).pristineForm();
    }

  }])
  .directive("myFormDirective", ["directiveHandler", function(directiveHandler) {
    return {
      restrict: "E",
      template: `<div>
                  <form name="myForm">
                    Username : <input type="text" name="username" ng-model="username" required/>
                    <div ng-show="myForm.username.$dirty && myForm.username.$invalid">
                      Please fill in username.
                    </div>
                    <div>Form $pristine : {{myForm.$pristine}}</div>
                  </form>
            </div>`,
      replace: true,
      scope: {
      },
      link: function($scope, $elm, $attrs, $ctrl) {
        var directiveNameAttr = $attrs.dirName ? $attrs.dirName : "myFormDirective";
        directiveHandler.registerDirective(directiveNameAttr, $scope);

        $scope.username = "";
        $scope.pristineForm = function() {
          $scope.myForm.$setPristine();
        }
      }
    };
  }]).factory('directiveHandler', function() {
    var instance_map = {};
    var service = {
      registerDirective: registerDirective,
      getDirective: getDirective,
      deregisterDirective: deregisterDirective
    };

    return service;

    function registerDirective(name, ctrl) {
      instance_map[name] = ctrl;
    }

    function getDirective(name) {
      return instance_map[name];
    }

    function deregisterDirective(name) {
      instance_map[name] = null;
    }
  });

HTML :
<div ng-app="app" ng-controller="controller as ctrl">
  <my-form-directive dir-name="myFormDir"></my-form-directive>
  <div ng-click="ctrl.pristineForm('myFormDir')"><button>Set Form Pristine</button></div>
  
  <my-form-directive dir-name="myFormDir2"></my-form-directive>
  <div ng-click="ctrl.pristineForm('myFormDir2')"><button>Set Form Pristine</button></div>
</div>



說明:
在 directiveHandler 中,設定了一個內部管理的 instance_map,
用 key-value 的次式將需要的 directive instance (可能是 isolate scope 的 scope,可能是 directive 設定的 controller 等,依情況自行決定) 存起來,
設定了三個 function,

主要的為  registerDirective(name, ctrl) 和 getDirective(name),
registerDirective(name, ctrl) 主要在 directive 初始化時使用一次,
用想要的 name 作為 key,把想要暴露給外部使用的 instance 作為 ctrl 設定進來。

getDirective(name) 在外部使用,例如此例的 controller,
用 name 值來取得 directive 的內部 instance,例如此例即為 <my-form-directive> 的 isolate scope,
取得 instance 後,即可執行 instance 上所設定的 function,當然取得 instance 上設定的值也是可以的。

在 html 中使用 <my-form-directive> 時,給了一個 dir-name 屬性,
dir-name 屬性在 directive 用來作為 name 參數呼叫 registerDirective(name, ctrl),
如果有多個 directive 的話,為了區分各個 directive,
給定不同的 name 作為 instance 在 directiveHandler 中的 key 值將會
使管理各個 directive 的 instance 更為方便。

為了演示,我們在 html 中使用了兩個 <my-form-directive>,並給它們設定了不同的 dir-anem 屬性值。

參考資料:

  1. How to call a method defined in an AngularJS directive? (最喜歡Mudassir Ali的回答)

2017年5月29日 星期一

AngularJS的UI-Router練習

今天要來練習AngularJS很多人使用的外掛,UI-Router,UI-Router以各種不同的 State (狀態) 來管理AngularJS的路由,擴充了AngularJS原生路由的使用功能 (只單純以Url來組織路由),所以網站是以一個State到另一個State的方式去設計。

以下是今天演示的需求:

  1. 設計三個State, home、heroPanel、heroDetail,
    home為首頁,heroPanel顯示英雄(hero)的 id 和 name 的列表,點列表的某一個 hero 會進到heroDetail,顯示 Detail 頁面。
  2. 頁面結構分成三個區塊,header、body和footer,三者的內容皆會跟據State而有所改變。

成品就像下面影片這樣:




首先先來看一下設計的檔案結構:

index.html為主要頁面,且此例也只有一個頁面,並在同一頁中利用UI-Router切換內容。

因為這邊我用npm的方式下載安裝AngularJS和UI-Router,所以有package.json,AngularJS和UI-Router都裝在node_modules裡,當然自己去官網下載也OK。

app資料夾下的為主要JS程式,第一層以app開頭的JS為主要AngularJS Module及其相關設訂,代表著 "home" 的狀態。
其他的資料夾, heroPanel 和 heroDetail 代表 heroPanel 和 heroDetail 兩個狀態,為ui-view是body時的內容設定。
common  資料夾下放著跟 footer 和 header 兩個 ui-view相關的設定

main.css 為 header、 body、footer 標上外框以利識別,也為 class="active" 的元素標上底色以利識別現在狀態。

接下來來看各檔案裡的程式內容

2017年5月12日 星期五

AngularJS 自製有Service、directive的外掛module練習

今天要來製作一個自製的AngularJS外掛,用來練習AngularJS的模組化特性。

要實作的是一個簡單的Lightbox (或Modal),要練習的點是:

  1. 此外掛為一個Module,可引入至其他的AngularJS Module。
  2. 此Module提供一個Service,可以由引入此Module的程式呼叫使用。
  3. 此Module提供一個Directive,Directive可以與參數雙向(或單向,看需求)綁定,並且Directive可以呼叫上述Service來使用。

目標結果為:

  1. Lightbox外掛提供一個Service,可以開始及關閉Lightbox,並且可以在開啟Lightbox時提供要顯示在Lightbox中的文字。
  2. Lightbox外掛提供一個Directive,Directive經過設計,有提供一個按鈕,其被按下會執行Service的開啟lightbox功能,Directive可以給入參數,設定Lightbox中要顯示的文字。
  3. 開啟的Lightbox提供一個Close按鈕,按下可以關閉Lightbox。
  4. 一次只會有一個Lightbox被開啟。

其成品就像如下視頻那樣:


首先先 看一下檔案結構:


  1. /index.html:主要的網頁頁面。
  2. /js/index.js :index.html使用的JS,主要頁面的AngularJS邏輯。
  3. /js/angular.js : 要引入使用的angularjs。
  4. /js/myAngularJsPlugin.js : 要撰寫的自製Lightbox AngularJS Plugin。
程式碼如下,相關的說明都已經寫在註解中了:
  1. /index.html :
    <!DOCTYPE html>
    <html>
        <head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
            <script src="js/angular.js"></script>
            <script src="js/myAngularJsPlugin/myModalModule.js"></script>
            <script src="js/index.js"></script>
        </head>
        <body>
            <div ng-app="myApp" ng-controller="myController as myCtrl">
                可自行輸入要在Modal內顯示的文字
                <div style="margin-bottom: 20px;">
                    <input type="text" ng-model="myCtrl.modalText"/>
                </div>
                在Controller裡呼叫自製Module提供的Service來開啟Modal
                <div style="margin-bottom: 20px;">                
                    <button ng-click="myCtrl.openModal(myCtrl.modalText)">Open Modal manually</button>
                </div>
                
                使用自製Module提供的Directive來開啟Modal
                <div>                
                    <my-modal-directive modal-text="myCtrl.modalText"></my-modal-directive>
                </div>
            </div>
        </body>
    </html>
  2. /js/index.js :
    angular.module("myApp", ["myModalModule"]);
    
    angular.module("myApp").controller("myController", ["myModalService", function(myModalService){
        var self = this;
        self.modalText = "測試文字";
        self.openModal = openModal;
        
        function openModal(modalText){
            myModalService.open(modalText);
        }
    }]);
  3. /js/myAngularJsPlugin.js:
    //Module
    angular.module("myModalModule", []);
    //Service
    angular.module("myModalModule").factory("myModalService", ["$rootScope", "$compile", function ($rootScope, $compile) {
            //設計一次只能有一個Modal開啟
            var scope;
            var modal;
            
            var $body = angular.element(document).find('body');
            var isModalOpened = false;
            //modal template中有跟angularjs scope有關的程式碼,
            //須要使用$compile編譯並與scope連結才能有作用
            var modalTemplate = '<div style="position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background-color: rgba(158, 158, 158, 0.68); ">' +
                                    '<button ng-click="close()" >Close</button>' +
                                    '<div style="display: flex; align-items: center; justify-content:center; width: 100%; height: 100%;">' +
                                        '{{textContent}}' +
                                    '</div>' +
                                '</div>';
            //functions
            function open(textContent) {
                if (!isModalOpened) {
                    //創建暫時的scope
                    scope = $rootScope.$new();
                    //在scope中設置close函式
                    scope.close = close;
                    //在scope中設置textContent
                    scope.textContent = textContent;
                    //使用$compile來編譯modalTemplate,得到link函式,執行link函式並代入scope來與scope連結,
                    //最後得到連結好的DOM
                    modal = $compile(modalTemplate)(scope);
                    //將得到的DOM加進<body>中
                    $body.append(modal);
                    //設置modal opened狀態
                    isModalOpened = true;
                }
    
            }
            function close() {
                if (isModalOpened) {
                    //將Modal DOM移除
                    modal.remove();
                    //移除暫時scope
                    scope.$destroy();
                    //設置modal opened狀態
                    isModalOpened = false;
                }
            }
            //自製Service對外提供的API
            return {
                open: open,
                close: close
            };
        }]);
    //Directive
    angular.module("myModalModule").directive("myModalDirective", ["myModalService", function (myModalService) {
            return {
                restrict: "E",
                replace: true,
                scope: {
                    //自製Directive可接受modalText參數(為表達示)
                    modalText: "=modalText"
                },
                template: generateTemplate,
                link: function ($scope, $elm, $attrs, $ngModel) {
                    $scope.openModal = myModalService.open;                
                    /* 以上寫法等同以下,上面雖簡潔但下面寫法可讀性比較好
                    $scope.openModal = function (modalText) {
                        myModalService.open(modalText);
                    }
                    */
                }
            };
    
            function generateTemplate($elm, $attrs) {
                 //用function回傳template,也可以直接用字串不用函式
                return '<button ng-click="openModal(modalText)">Open Modal by Directive</button>';
            }
        }]);
原始碼下載:
angularJS-servicePractice.7z


參考資料:
  1. 深入解析 CSS Flexbox

2016年12月18日 星期日

AngularJS中使用$http的POST不要傳送JSON格式給JSP, Servlet的方法

AngularJS 在用$http的angularjs service傳POST時,參數預設會用JSON的格式傳至url指定的後台,例如如果用以下的傳送方式:
$http({
   method: 'POST',
   url: 'xxx.do',
   data: {param1: "param1", param2: "param2"}
  })

後台例如Servlet會得到一串JSON格式的文字,像是 {'param1':'param1, 'param2' : 'param2},
但通常這並不是我們想要的key-value的型式,如果想要傳遞key-value型式的話,必須要用到
$httpParamSerializer這個angularjs service,並且設定header (不設定的話預設的Content-Type是application/json;charset=utf-8),使用方法如下:

$http({
   method: 'POST',
   url: 'xxx.do',
   data: $httpParamSerializer({param1: "param1", param2: "param2"}),
   headers: {
       'Content-Type': 'application/x-www-form-urlencoded'
   }
  })


這樣AngularJS就會以param1=param1&param2=param2的方式傳至後台了。

2016年8月20日 星期六

使用Ajax上傳檔案 - (Javascript, JQuery or AngularJS) + FormData

在以前我們要在網頁中傳送檔案資料時,通常會需要設計一個html Form,並設定Form的encype=multipart/form-data和準備一個input type="file",在User按下submit按鈕後,跟據Form
所設定的action="URL",將整個頁面request移動到action所指定的地方,例如Servlet,
等處理完後在將User導至其他網頁頁面。

但有沒有可能不要讓User被導到其他頁面,留在原頁面就完成檔案資訊的傳送呢?
答案是可以的,這邊就要利用新的javascript類別FormData和Ajax的技術來達到Ajax傳
送檔案(也可順便傳遞其他input資料)資料。


在這邊我們要利用Netbeans、Servlet3.0、Tomcat 8來實做我們的範例,
實現了三個版本的檔案上傳:Javascript、JQuery、AngularJS
首頁是專案檔案結構,如下圖:

所用的版本JQuery為v2.0.3、AngularJS為v1.5.8,
主要的重要檔案有:

  1. fileUploadAjaxExample.html
    給User上傳檔案的html網頁。
  2. FileUploadAjax.js
    處理檔案資料上傳的Javascript File.
    包含Javascript、JQuery、AngularJS三個版本。
  3. FileUploadAction.java
    用來接收檔案資料的servlet,這裡設定URL patter為/fileUpload.do
  4. web.xml
    設定檔,其中必須在要接收檔案的servlet中設定的tag,
    tag裡可以有以下的子tag設定:
    <location>         檔案存放位置 (使用Part.write(fileName)可以在寫入檔案,但如果fileName
                               為絕對路徑則以絕對路徑為準)
    <max-file-size>  最大檔案size
    <max-request-size>   最大request size  (例如POST的request size)
    <file-size-threshold>
       超過file-size-threshold的檔案request將會以臨時暫存的方式存到硬                                       碟中,預設為0
接下來我以來各別看下每個實作檔案的內容:

2016年8月13日 星期六

自製檢查file size的directive (input type="file") - AngularJs

AngularJS的表單、欄位驗證(Validation)非常好用,但有時會碰到想要自訂驗證方式、
或是某個欄位AngularJS並沒有實作Validation時(例如input type="file"),
就需要自訂有客製化驗證能力的directive

在這邊的需求如下:

  1. 製作一個directive,名稱為file-validator
  2. 配合input type="file"使用
  3. 可以設定file size(大小,單位Byte)和file type(副檔名), 用法範例:
  4. 如果fileSize沒指定或小於等於0,則不對fileSize做限制驗證。
  5. 如果fileType沒指定或為空字串,則不對fileType做限制驗證。
完成的程式碼成品如下:


說明:

  1. 當我們在ng-form裡面的input設置name及ng-model (及ngModelController)後,如果AngularJS有實作此種input type類型的話,Angular會在View中的值或對應model的
    值改變時,進行View及model的雙向挷定、同時變更。
    ngModelController裡面會存放model的值,即ngModel.$modelValue。
    也會存放view(通常為input type中顯示的值),即ngModel.$viewValue。
    並且也會管理此input的valid狀態。

    但因為AngularJS並沒有對input type="file"進行實作,也就是,不管User在input中選了什麼檔案,皆不會存值到ngModel.$modelValue及ngModel.$viewValue中,當然雙向挷定的model中也不會有值。

    所以為們要帶自制的directive中,指定
    required : ngModel
    來得到管理這個input的ngModel,並且手動的設定model的值。
    (!!不要手動用ngModel.$setViewValue()設定viewValue的值,會導致model value 會被蓋掉)
  2. 因為要設定model的值,所以我們要引入$parse,$parse(code)可以代入一串程式碼,有點
    像eval,$parse(code)會返回一個函式,並且此函式有一個函式,assign(scope, value),
    可以在scope中執行程式碼,並將value賦值給程式碼執行得到的變數。
    例如:
    $parse("myCtrl.fileUpload").assign(scope, file);
    代表在scope中執行 :  myCtrl.fileUpload = file;
  3. 在ngModel中,可以使用
    ngModel.$setValidity( validType, isValid)
    來設定此input的validType是valid還是invalid。
  4. 在解析如以下字串時,
    file-validator="fileType:'png'; fileSize:333;"
    我們使用了兩個正規表達式:
    fileSize\s*:\s*(\d+)\s;

    fileType\s*:\s*(["'])(\w+)\1\s*;
    其中第二個正規達式中的 \1 的意思是跟第一個Group相匹配,而第一個Group就是(["']),
    也就是fileType:"png"; 或 fileType: 'png';可以匹配,但
                fileType:'png";  或 fileType:"png'; 不可以匹配,
    雙引號及單引號要成對使用。

原碼紀錄(怕jsFiddle出問題):
使用angular.js
html:

請選擇size小於3MB的PNG檔案


file valid: {{myForm.fileUpload.$valid}}
file size (Byte): {{myCtrl.fileUpload.size}}
副檔名錯誤
檔案過大


javascript:
 var myApp = angular.module("myApp", []);

 myApp.controller("myController", [function() {
   //
 }]);
 //自製的file validator
 myApp.directive('fileValidator', ["$parse", function($parse) {
   return {
     restrict: 'A',
     scope: true,
     require: 'ngModel', //代表管理input type="file"這個input的ngModelController,例如此例
     //ngModel.name = "fileUpload",
     //可以連結ng-model裡指定的變數值(ngModel.modelValue) 和
     //input view欄位中顯示的值(ngModel.viewValue)                          
     link: function(scope, elm, attrs, ngModel) {
       var expression = attrs.fileValidator;

       var fileSizeReg = /fileSize:\s*(\d+)\s*;/;
       var fileTypeReg = /fileType\s*:\s*(["'])(\w+)\1\s*;/;

       //規定file的size,單位Byte
       var fileSizeLimit = fileSizeReg.exec(expression)[1] || 0;
       //規定副檔名
       var fileTypeLimit = fileTypeReg.exec(expression)[2] || "";

       elm.bind('change', function() { //發現input type file值改變時
         scope.$apply(function() {
           var file = elm[0].files[0]; //取得file資料
           var fileType = /.+\.(.+)/.exec(file.name)[1];

           //$parse("程式碼")可以返回一個function,之後可以用這個function的
           //assign(scope, value)來在scope中進行賦值(value)動作,
           //例如: 在scope中墸行: 程式碼 = value (有點像eval())
           $parse(attrs.ngModel).assign(scope, file);

           //檢查file副檔名及size
           //用ngModel設置fileSize的valid
           if (fileTypeLimit === "" || fileType === fileTypeLimit) {
             ngModel.$setValidity("fileType", true);
           } else {
             ngModel.$setValidity("fileType", false);
           }
           //用ngModel設置fileSize的invalid
           if (fileSizeLimit <= 0 || file.size < fileSizeLimit) {
             ngModel.$setValidity("fileSize", true);
           } else {
             ngModel.$setValidity("fileSize", false);
           }
         });
       });
     }
   };
 }]);

2016年6月8日 星期三

AngularJS - 自製跟ng-repeat一樣能力的directive (my-repeat)

在這篇文中,我們要來製作一個跟AngularJS的ng-repeat有幾乎相同能力的自製directive(當然ng-repeat更完整,有更多的可用變數、接受表達示等)。

我們先定義出自製directive要擁有的能力,在這裡我們取名我們的directive叫作my-repeat:
  1. 使用型式如同ng-repeat,可用attribute的型式放在html element tag的attribute中使用。
  2. 其中,屬性裡面可以用 "XX in YYY" 這種型式的表達示來表示要對哪個array型式(YYY)的內部資料(XX)做迴圈。
  3. 在directive中,可以解析YYY中的XX來顯示資料,也可解析其他資料,例如controller中的變數。
  4. 能夠使用 "-start" 和 "-end" 的multiElement的用法。
  5. 此directive要可以嵌套到另一個directive中(其他的directive或自己)。

下面就是實現出的程式碼,可以在Result看到,我們的my-repeat擁有和ng-repeat相同的上述能力。(**有使用jQuery做$(element)和before()的動作)


在程式碼中已經寫了詳細的註解,不過我們還必須對執行完上述程式碼後,AngularJS產生出了什麼樣的Scope關係。
在程式碼裡,我們已經在有scope出現的地方進行了console.log()的輸出,可以由其中的屬性物件、$id、_proto_了解到scope之間的關係,以下已經用簡單圖示來表示從console.log()結果中看出的關係:

2016年4月18日 星期一

ng-repeat-start和ng-repeat-end -- AngularJS

在AngularJS中,我們常會用ng-repeat這個directive來將一串資料顯示出來,不過ng-repeat的功能是有局限性的,例如如果我們有以下的data,

datas = [{name : 1 , phone:111},{name : 2 , phone:222},{name : 3 , phone: 333}];

並想讓以下重覆顯示的話,ng-repeat就不夠用了。

<h1>Name : {{data.name}}</h1>
<p style="border-bottom:thick solid #ff0000;">Phone : {{data.phone}}</p>

這時我們可以使用ng-repeat-start及ng-repeat-end,

在ng-repeat-start標示的tag及ng-repeat-end標示的tag之間(包括被ng-repeat-start和ng-repeat-end標示的tag),能夠存取到同一個串列資料中的各元素,下面就是一個使用的簡單例子:


我們先定義了ng-app、ng-controller及其中的datas。

var myapp = angular.module('myapp',[]);
myapp.controller('Ctrl',[function(){
 var self = this;
  self.datas = [{name : 1 , phone:111},{name : 2 , phone:222},{name : 3 , phone: 333}];
}]);

接著在設計要顯示的html:

<div ng-app="myapp" ng-controller="Ctrl as ctrl">  
  <h1 ng-repeat-start = "data in ctrl.datas">Name : {{data.name}}</h1>
  <p ng-repeat-end style="border-bottom:thick solid #ff0000;">Phone : {{data.phone}}</p>  
</div>

可以看到每一次的repeat都從串列資料(datas)中取出一組資料(包括了name及phone的資訊),並顯示一組
<h1>......</h1>
<p>.......</p>

參考資料:

  1. ngRepeat

2016年4月13日 星期三

AngularJS的$compile() - 動態增加DOM

在AngularJS中,會在瀏覽器對html解析成DOM後,先對AngularJS(ng-app)的進行編譯,給定scope後才會進行數據綁定。

但是如果是用例如JQuery等方式新增了新的DOM元素後,因為沒有用AngularJS編譯的關係,所以就算裡面有AngularJS的語法也不會呈現預期的結果。

例如像以下的例子:

HTML:
<div ng-app="myApp">
  <div id='DOMWrapper' ng-controller="myCtrl">
    <button ng-click='addDOM()'>Add DOM</button>
  </div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', '$compile', function($scope, $compile) {
	$scope.textOfDOM = 'Text show successfully';

  $scope.addDOM = function() {
    var DOMToAdd = $('<p>{{textOfDOM}}</p>');
    
    //Wrong code -- start
    $('#DOMWrapper').append(DOMToAdd);
    //Wrong code -- end    
    
    //Right code -- start
    //var linkOfDOMToAdd = $compile(DOMToAdd);
    //var nodeOfCompiledDOM = linkOfDOMToAdd($scope);
    //$('#DOMWrapper').append(nodeOfCompiledDOM);
    //Right code -- end
  };
}]);

 從結果中可以看到當按下"Add DOM"按鈕時,出現的字是 "{{textOfDOM}}",而不是我們預期的 "Text show successfully"。

這是因為要新增的DOM沒有被AngularJS編譯的關係,以下才是正確的程式碼:
HTML:
<div ng-app="myApp">
  <div id='DOMWrapper' ng-controller="myCtrl">
    <button ng-click='addDOM()'>Add DOM</button>
  </div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', '$compile', function($scope, $compile) {
	$scope.textOfDOM = 'Text show successfully';

  $scope.addDOM = function() {
    var DOMToAdd = $('<p>{{textOfDOM}}</p>');
    
    //Wrong code -- start
    //$('#DOMWrapper').append(DOMToAdd);
    //Wrong code -- end
    
    //Right code -- start
    var linkOfDOMToAdd = $compile(DOMToAdd);
    var nodeOfCompiledDOM = linkOfDOMToAdd($scope);
    $('#DOMWrapper').append(nodeOfCompiledDOM);
    //Right code -- end
  };
}]);
我們可以呼叫$compile()並把要編譯的DOM送進去當參數,它會返回一個 link 函式,呼叫 link 函式並把scope當作輸入參數,就可以將scope與被編譯的DOM產生連結,所以DOM就可以得到textOfDOM的值,link 函式會返回已經跟資料連結好的新的DOM物件,接著我們再把新的DOM物件用JQuery的語法放到 #DOMWrapper裡面,就可以正確的得到我們要的 "Text show successfully"了。

參考資料:

  1. 18.5. Compile的细节
  2. $compile