在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/