示例可重用的web component方式組織angular應用模組

世有因果知因求果發表於2015-05-17

在online web應用中,經常有這樣的需求,能夠讓使用者通過瀏覽器來輸入程式碼,同時能夠根據不同的程式碼來做語法高亮。大家已知有很多相應的javascript庫來實現語法高亮的功能,比如codemirror就是一個不錯的選擇。而我們使用angular開發web應用,那麼就希望能夠使用directive來實現這個功能,以便能夠很好的重用。https://www.polymer-project.org/ 可以得到更多可重用web元件的資訊

下面是如何使用我們的  <code-editor>  的:

<html ng-app="demo">
  <head>
    <title>Posts</title>
  </head>
  <body>
    <code-editor syntax="javascript" ng-model="content.code">
      var sum = function (a, b) {
        return a + b;
      }
    </code-editor>
    <textarea ng-model="content.code" cols="100" rows="50"></textarea>
  </body>
</html>

這個directive的目標是:像 <pre> 元素一樣接受接受輸入程式碼文字,同時能夠和 <ngModel> 一樣將輸入文字作為model放到變數中去。該directive同時也需要將程式碼在codemirror程式碼編輯器中展示。

首先我們來建立基本的資料夾結構。我們建立一個components/codeEditor目錄作為我們整個code editor元件的工作目錄。在這個目錄中,我們將該元件所用到的javascript庫(實際就是codemirror庫)以及css檔案放到vendor目錄中。建立一個新的codeEditor.js作為directive程式碼,一個code-editor.html模板也放在component根目錄中。

使用這種方式來組織程式碼實際上就是在遵循web component開發的模式了。現在在code-editor.html模板檔案的div元素上初始化codeMirror作為開始。

code-editor.html:

<div class="code-editor"></div>

codeEditor.js:

angular.module("demo").directive("codeEditor", function(){
  return {
    restrict: "E",
    replace: true,
    scope: {
      syntax: "@",
      theme: "@"
    },
    templateUrl: "components/codeEditor/code-editor.html",
    link: function(scope, element, attrs) {
      var editor = CodeMirror(element[0], {
        mode: scope.syntax || "javascript",
        theme: scope.theme || "vibrant-ink",
        lineNumbers: true
      });
    }
  };
});

在上述js程式碼中,呼叫codeMirror函式使得div元素具有codeMirror的程式碼高亮功能,同時lineNumber顯示出來。這時已經具有了程式碼輸入和高亮的功能:

這時程式碼編輯器已經工作了,但是你發現我們在使用code-editor directive的html檔案中的程式碼卻並沒有被渲染。這是因為,預設情況下,angular將丟棄code-editor元素的內容,替而代之為該directive的template:code-editor.html的內容。為了讓angular能將包含在原始code-editor元素內的內容儲存並使用起來,我們需要設定code-editor元素的transclude屬性為true.

另外一點是:我們不準備使用 ng-transclude directive來插入內容。反而,我們將執行一個手動的transclude,以便允許內容直接傳遞到editor。下面是相關程式碼:

codeEditor.js:

angular.module("demo").directive("codeEditor", function(TextUtils){
  return {
    restrict: "E",
    replace: true,
    transclude: true,
    scope: {
      syntax: "@",
      theme: "@"
    },
    templateUrl: "components/codeEditor/code-editor.html",
    link: function(scope, element, attrs, ctrl, transclude) {
      var editor = CodeMirror(element[0], {
        mode: scope.syntax || "javascript",
        theme: scope.theme || "vibrant-ink",
        lineNumbers: true
      });
      
      transclude(function(clonedEl){
        var initialText = TextUtils.normalizeWhitespace(clonedEl.text());
        editor.setValue(initialText);
      });
    }
  };
});

上述程式碼中,當transclude屬性被設定為true,我們在link函式中就有了第5個引數--transclude函式。該函式有幾種不同的用法,在我們的例子中,我們將傳入一個callback函式作為第一個引數。這個callback被呼叫時將被傳入一個原始transcluded content作為引數,這樣我們就可以訪問到那塊內容並且被傳遞給editor去。TextUtils是一個簡單的服務,半酣一個normlizeWhitespace函式,該函式可以清除leading whitespace.

最後一件事情是我們需要讓ngModel可以和editor工作起來,這樣我們就可以將他和input,textarea等繫結起來。第一步,我們需要require ngModel屬性。

angular.module("demo").directive("codeEditor", function(TextUtils){
  return {
    restrict: "E",
    replace: true,
    require: "?ngModel",
    transclude: true,
    scope: {
      syntax: "@",
      theme: "@"
    },
    templateUrl: "components/codeEditor/code-editor.html",
    link: function(scope, element, attrs, ngModelCtrl, transclude) { ... }
  };
});
  
// ....

通過設定require屬性,我們告知這個directive從ngModel中包含controller的引用到link函式中。通過增加一個?在directive名稱前,我們意味著這是一個選項,以便阻止angular在ngModel並不存在的情況下丟擲異常。既然我們在directive的link函式中已經可以訪問ngModel的controller了,我們需要了解幾個用於同步資料到model的幾個函式:

  • ngModelCtrl.$setViewValue(someValue) 

$setViewValue函式用於在model上賦值。當editor的內容變化時,我們需要呼叫該函式。

  • ngModelCtrl.$render

$render屬性需要被賦一個函式值。這個函式是一個當model value變化時呼叫的callback。注意model的value並不會傳遞到這個函式中去。一旦model的value發生變更時,我們需要設定editor的內容為model的value

  • ngModelCtrl.$viewValue

$viewValue屬性包含model的當前值。每當$render回撥被呼叫時我們需要使用它設定editor的value。

看看下面的程式碼:

angular.module("demo").directive("codeEditor", function($timeout, TextUtils){
  return {
    restrict: "E",
    replace: true,
    require: "?ngModel",
    transclude: true,
    scope: {
      syntax: "@",
      theme: "@"
    },
    templateUrl: "components/codeEditor/code-editor.html",
    link: function(scope, element, attrs, ngModelCtrl, transclude){
      var editor = CodeMirror(element[0], {
        mode: scope.syntax || "javascript",
        theme: scope.theme || "default",
        lineNumbers: true
      });

      if(ngModelCtrl) {
        $timeout(function(){
          ngModelCtrl.$render = function() {
            editor.setValue(ngModelCtrl.$viewValue);
          }
        })
      }

      transclude(function(clonedEl){
        var initialText = TextUtils.normalizeWhitespace(clonedEl.text());
        editor.setValue(initialText);

        if(ngModelCtrl){
          $timeout(function(){
            if(initialText && !ngModelCtrl.$viewValue){
              ngModelCtrl.$setViewValue(initialText);
            }

            editor.on('change', function(){
              ngModelCtrl.$setViewValue(editor.getValue());
            });
          });
        }
      });

      scope.$on('$destroy', function(){
        editor.off('change');
      });
    }
  }
});

  當model變化時,既然我們使得require ngModel為可選而非必選,我們需要確保ngModel controller是否確實存在。在$render函式中,我們將model的value使用$viewValue屬性賦值到editor的contents中去。這將避免一個Undefined value被設定到editor中去。

每當editor變更時,我們設定model的value。同時我們使用一個$timeout來呼叫它是因為為了等待model初始化。

最後我們刪除和editor關聯的用於更新model的event handler。當你使用非angular事件時,他們需要在$destroy回撥中被清除。這將確保directive完全清除避免memory leak。

https://www.codeschool.com/blog/2015/03/06/digging-advanced-angularjs-directives/

 

相關文章