Quantcast
Channel: 黑暗執行緒
Viewing all 2456 articles
Browse latest View live

【茶包射手日記】ASP.NET MVC CSS壓縮失效

$
0
0

用Chrome瀏覽啟用JavaScript/CSS打包壓縮的ASP.NET網站,發現以下錯誤:

Chrome抱怨找不到sytle.css.map檔! (style.css由SCSS編譯產生,.map檔是所謂的Source Map,用來查詢.css特定段落所對應的.scss原始碼,Debug不可或缺。當今主要的CSS與JavaScript編譯語言都提供.map方便偵錯,例如: LESS、CoffeeScript、SASS/SCSS,連TypeScript也不例外,未來再介紹)

檢查bundle/css?v=…,在結尾真的找到sourceMappingURL註記。但不太對勁! CSS在壓縮後所有註解應被移除,怎麼會還留下map連結的註解? 看到檔案開頭處,答案揭曉:

打包壓縮程式遇到CSS語法出錯壓縮失敗時,會傳回原始版本,而錯誤訊息列在下方:
(1600,5): run-time error CSS1062: Expected semicolon or closing curly-brace, found '@inlcude'

@inlcude, @inlcude, @inlcude, @inlcude, @inlcude… 啊啊啊啊~ 氣到想剁手指!

平撫激動情緒,回到SCSS修正錯誤,問題排除。


NG筆記2-網頁MVVM基本架構

$
0
0

MVVM是前端Framework的重要功能,AngularJS當然也有強大好用的資料繫結及模板(Template)機制,才能橫掃江湖。我們就從最簡單的"計算型屬性"開始,順便介紹Angular程式的基本架構。
(註: AngularJS採取的應該是MVC設計模式,但依我的理解,資料繫結、模板、屬性邏輯寫在Scope等特性,與MVVM設計模式沒有分別,因此使用Angular處理網頁呈現時,我仍然會用MVVM的思維看待它。)

KO範例1 - 計算型屬性為示範: ViewModel有firstName及lastName兩個屬性,第三個屬性fullName等於fisrtName與lastName相加。這是再平常也不過的應用,對NG來說是小菜一碟: Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 1 - 計算型屬性</title>
</head>
<body>
<divng-controller="mainCtrl">
<inputtype="text"ng-model="firstName"/>
<inputtype="text"ng-model="lastName"/>
<br/>
<spanng-bind="fullName()"></span>
<br/>
<span>{{ fullName() }}</span>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("mainCtrl", function($scope) {
      $scope.firstName="Jeffrey";
      $scope.lastName="Lee";
      $scope.fullName = function() {
return $scope.firstName + " " + $scope.lastName;
      };
    });
</script>
</body>
</html>

程式解析:

  1. <html>加上ng-app,指明網頁要使用sampleApp這個Module。(依慣例用App做為Module名稱字尾)
  2. <div>加上ng-controller,指明由mainCtrl這個Controller主控<div>內的MVVM作業。(依慣例取Ctrl字尾)
  3. <input>加上ng-model="屬性",標明對firstName及lastName屬性做雙向繫結。
  4. 單向繫結有兩種寫法: 用ng-bind或是直接寫{{ propName }},後者簡潔直覺,可寫成"如果{{fact}}已成事實,{{action}}就是義務"一般文字與繫結內容交雜的字串,但缺點是繫結完成前要防止使用者看到{{、}}等裸碼。
  5. 網頁要載入angular.js。
    NG內建jqLite,摸擬並借用基本的jQuery功能以處理DOM操作,但功能很陽春。如果想要用完整的jQuery功能,可在載入angular前先載入jQuery,jqLite就會換成jQuery。我個人沒有jQuery就混不下去,專案中一定會加,此處不用僅示範NG不依賴jQuery也能運作。
  6. 程式用angular.module("sampleApp", [])建立並註冊一個新的Module。注意後方的[]空集合表示不需要參照其他Moudle,但不能省略,若寫成.module("sampleApp"),NG會假設sampleApp已存在試圖找尋建好的Instance而出現錯誤。
  7. .module()方法傳回Module物件,像jQuery一樣,可接著呼叫.controller(名稱, 建構式)註冊Controller。(還可呼叫.service()、.directive()、filter()…等註冊其他項目)
  8. Controller建構式參數透過DI(依賴注入)方式傳遞,故把用到的項目列上去即可,順序不重要。我們一定會用到的是$scope,它相當於KO的ViewModel角色,MVVM沒有VM玩個屁呢?
    註: function($scope) { ... }的寫法在JavaScript壓縮(Minification)過程可能被修改,$scope被換成a, b之類的隨機變數名稱,導致DI機制失效。因此NG提供另一種寫法,.controller("mainCtrl", ["$scope", function($scope) { ... }],如果要進行JS壓縮,記得要用這種寫法。(關於Controller的宣告方式,保哥有篇文章詳解)
  9. $scope.firstName宣告ViewModel有個firstName屬性,事實上不加也成,NG在ng-model看到未宣告屬性,便會自動在$scope新增。(不必宣告,遇缺新增的缺點是打錯字不會報錯而是冒出新屬性,Debug起來比較刺激)
  10. 在KO要用ko.observable()屬性才可雙向繫結,需用ko.computed()建立計算型屬性。在NG不必這麼麻煩,直接寫成一般JavaScript物件跟屬性處理就可以,所以fullName寫成function() { return this.fisrtName + " " + this.lastName; },在firstName或lastName變動時就會自動更新。(註: 這麼做有其方便之處,但複雜情境不易判斷某項更動會不會影響繫結,與KO的做法也算各有優劣)

就這樣,第一個範例完成了。

實務上,我習慣把真正的ViewModel獨立出來,變成事先定義好的function,如同在KO裡"先宣告function myViewModel() { var self = this; … },再ko.applyBindings(new myViewModel())"一樣。

這麼做有三個目的:

  1. 將ViewModel類別自$scope抽離,以便用程式產生器依文件或C#類別產生對應的ViewModel,在大型專案中有其必要性
  2. 中大型專案我傾向用TypeScript開發JavaScript,ViewModel獨立成function或類別後,方便用TypeScript寫成強型別類別或介面。
  3. $scope.propA在NG中常會因Scope繼承出現非預期結果,寫成$scope.model = { propA: "A" }較不易受影響。(Scope繼承議題挺複雜,先不多提,記得"資料放在$scope.model較不易被繼承給陰了"就好,如果對Scope繼承的雷想多了解一點,可以聽被陰過的索爾同學現身說法...)

因此,我心中理想寫法如下: Live Demo

<divng-controller="mainCtrl">
<inputtype="text"ng-model="model.firstName"/>
<inputtype="text"ng-model="model.lastName"/>
<br/>
<spanng-bind="model.fullName()"></span>
<br/>
<span>{{ model.fullName() }}</span>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("mainCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.firstName="Jeffrey";
        self.lastName="Lee";
        self.fullName = function() {
return self.firstName + " " + self.lastName;
        };
      }
      $scope.model = new myViewModel();
    });
</script>

像KO裡的做法一樣,我先宣告myViewModel(),再$scope.model = new myViewModel(),而ng-model則要改用model.firstName, model.lastName, model.fullName()。在更正式的應用裡,function() myViewModel會放在獨立JS檔維護管理。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記3-使用TypeScript

$
0
0

用Angular維護的SPA專案重頭戲都在前端,為避免JavaScript愈寫愈亂,失控長成哥吉拉的災難上演,決心在專案改用TypeScript,期望靠著強型別編譯檢查保平安。這裡就以上一篇文章的簡單NG程式為例,示範如何用Visual Studio 2013 Update 2 + TypeScript開發出相同的程式。

步驟1: 建立一個空白ASP.NET專案。(選空白(Empty)即可,WebForms, MVC, Web API都可以不用勾)

 

步驟2: 使用NuGet下載AngularJS及jQuery。

 

步驟3: 使用NuGet下載jQuery及Angular的TypeScript定義檔(TypeScript Definition,.d.ts) 用"angular tag:typescript"查詢,找到angularjs.TypeScript.Definitely.Typed安裝即可,它會透過相依關係一併裝好jQuery定義檔。


此時,在Scripts/typings應可看到jquery及angular兩個子目錄及多個.d.ts檔案

步驟4: 新增Scripts/vms/User.ts,宣告做為ViewModel的User類別。

/** 使用者基本資料型別 */
class User {
/** 名 */
    firstName: string;
/** 姓 */
    lastName: string;
/** 姓名 */
    fullName() : string {
returnthis.firstName + " " + this.lastName;
    }
}

順手加上JSDoc註解,如下圖所示,VS2013不但提供Intellisense,連中文說明都會一併帶出。

 

步驟5: 新增Scripts/ctrls/sampleApp.ts

/// <reference path="../typings/angularjs/angular.d.ts" />
/// <reference path="../vms/user.ts" />
angular.module("sampleApp", [])
    .controller("mainCtrl", ["$scope", function ($scope) {
var model: User = new User();
        model.firstName = "Jeffrey";
        model.lastName = "Lee";
        $scope.model = model;
    }]);

寫Controller時需引用Angular以及剛才定義好的User型別,故TS最上方要加入Angular定義檔及User TypeScript檔的參照。不要傻傻地用手敲,直接從Solution Explorer拖拉過去就成了,Visual Studio身為地表上最強開發工具,這種貼心設計是一定要的。

 

步驟6: 新增/default.html

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<title>Lab 1 - 計算型屬性(TypeScript版本)</title>
<scriptsrc="Scripts/jquery-2.1.1.js"></script>
<script src="Scripts/angular.js"></script>
<script src="Scripts/vms/user.js"></script>
<script src="Scripts/apps/sampleApp.js"></script>
</head>
<body>
<divng-controller="mainCtrl">
<inputtype="text"ng-model="model.firstName"/>
<inputtype="text"ng-model="model.lastName"/>
<br/>
<spanng-bind="model.fullName()"></span>
<br/>
<span>{{ model.fullName() }}</span>
</div>
</body>
</html>

HTML這邊沒什麼學問,只要將參考到的JS拉進來,包含User.ts及sampleApp.ts。記得寫<script src="…">時,副檔名要用.js而不是.ts,如果嫌麻煩,直接把.ts拖到HTML上,Visual Studio會自動插入<script>並聰明地將.ts改成.js。

 

就這樣,我們就成功用TypeScript寫出AngularJS前端囉~

最後來點洋葱! 用IE11開啟DevTools,在Debugger驚喜地發現IE11直接讓我們用user.ts及sampleApp.ts偵錯,可以直接在TypeScript程式設定中斷點,就跟用JavaScript偵錯一樣方便,很感動吧? (背後的魔法靠的是上回講過的Source Map)

 
[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記4-單元測試

$
0
0

自動測試是AngularJS架構的重要一環,官方文件有專章討論單元測試,程式庫則有ngMock提供單元測試所時的DI及Mocking(假物件模擬)支援。本篇將討論如何在Visual Studio 2013對Controller及ViewModel進行單元測試。

開始之前,Visual Studio要先安裝Chutzpah(發音類似"鬍子爸" XD)延伸套件,流浪小風有篇詳細介紹,在此不多贅述,直接切入Angular相關部分。我們就拿NG筆記2的簡單範例來練兵,為它建立單元測試。

首先,在Solution新增一個Class Libaray專案,並從NuGet安裝jasmine.js。(Jasmine是一套BDD精神的JavaScript測試Framework,AngularJS的單元測試及End-to-End(E2E)測試都用Jasmine寫測試腳本)

接著,在專案新增Tests資料夾,專門用來放置測試Script,測試檔可使用*.tests.js格式命名。整體的專案結構如下:

注意,T1.tests.js需以<reference>引用來自Web專案裡的JavaScript,Chutzpah會依照reference載入所需JS執行單元測試。如果希望編寫Jasmine Script階段能有完整Intellisense,可以連angular-mock.js、jasmine.js也一併納入參考。

T1.tests.js範例如下:

//測試集
describe("SampleApp測試", function () {
var scope, controller;
//beforeEach()內為每回測試前執行的程序
    beforeEach(function () {
//透過ngMock註冊sampleApp的相關設定,供隨後DI產生模組
        module("sampleApp");
    });
 
//子測試集
    describe("mainCtrl測試", function () {
//每個Spec(下方的it())執行前使用ngMock inject()產生Controller及Scope
        beforeEach(inject(function ($rootScope, $controller) {
            scope = $rootScope.$new();
            controller = $controller("mainCtrl", {
"$scope": scope
            });
        }));
//Spec 1
        it("初始firstName等於'Jeffrey'", function () {
            expect(scope.model.firstName).toBe("Jeffrey");
        });
//Spec 2
        it("fullName()由firstName及lastName組成", function () {
            scope.model.firstName = "Darkthread";
            scope.model.lastName = "Run"
            expect(scope.model.fullName()).toBe("Darkthread Run");
        });
    });
});

上述程式中的describe(), it(), expect()是Jasmine指令,Jasmine的指令沒幾個,官方文件看一遍就能上手,甚至不讀文件,直接看程式碼也不難望文生義。程式裡的module()、inject()是ngMock提供的功能,處理Module、Controller載入及Scope建立,透過DI可依需要注入假物件,而ngMock提供的$httpBackend假物件能模擬AJAX呼叫行為,確保前端程式在沒有伺服器時能也能單獨執行,這是形成單元測試的重要條件。每個it()通常用來印證一條"規格"、beforeEach()內的程式在每次跑it()前會先被執行,備妥測試環境。

Visual Studio安裝Chutzpah套件後,在T1.tests.js按右鍵可選擇直接執行測試或開啟瀏覽器檢視測試報表: (瀏覽器檢視時還能用Dev Tools偵錯,實務上不可或缺)

或者用Test Explorer選取測試執行也成:

介紹完用tests.js跑測試,該來談談TypeScript時代的單元測試,看看如何用TypeScript寫Jasmine測試?

首先在測試專案加入Jasmine的TypeScript定義檔:

新增TypeScript測試檔(T2.tests.ts),內容如下:

/// <reference path="../scripts/typings/jasmine/jasmine.d.ts" />
/// <reference path="../../web/scripts/typings/angularjs/angular-mocks.d.ts" />
/// <reference path="../../web/scripts/vms/user.ts" />
/// <reference path="../../web/scripts/apps/sampleapp.ts" />
 
/// <chutzpah_reference path="../../web/scripts/boo.js" />
/// <chutzpah_reference path="../../web/scripts/angular.js" />
/// <chutzpah_reference path="../../web/scripts/angular-mocks.js" />
/// <chutzpah_reference path="../../web/scripts/vms/users.js" />
/// <chutzpah_reference path="../../web/scripts/apps/sampleApp.js" />
 
describe("[TS]SampleApp測試", () => {
var scope, controller;
    beforeEach(module("sampleApp"));
    beforeEach(inject(function ($rootScope, $controller) {
        scope = $rootScope.$new();
        controller = $controller("mainCtrl", {
"$scope": scope
        });
    }));
    it("初始firstName等於'Jeffrey'", () => {
        expect(scope.model.firstName).toBe("Jeffrey");
    });
    it("fullName()由firstName及lastName組成", () => {
var model: User = scope.model;
        model.firstName = "Darkthread";
        model.lastName = "Run"
        expect(model.fullName()).toBe("Darkthread Run");
    });
});

重點在上方的參考宣告。測試TypeScript會用到Jasmine的describe(), beforeEach(),也會用到ngMock的module(), inject(),因此要納入jasmine.d.ts及angular-mocks.d.ts參考才能編譯,而測試要用到的Controller及ViewModel TypeScript,也需以<reference>加入參考。

TypeScript測試檔跟JavaScript測試檔的最大不同,在於<reference>加入*.ts是要讓TypeScript順利編譯,不等於執行期間要載入的JS項目,因此需要另外定義<chutzpah_reference>,取代原本JavaScript <reference>,讓Chutzpah知道要跑測試時載入哪些JavaScript檔案。所以在撰寫TypeScript測試檔時,除了參考TS,不要忘了用<chutzpah_reference>將需要的JS都列上去。

T2.tests.ts完成後,透過右鍵Run Tests及Test Exploer就能執行TypeScript所寫的測試檔。

提醒: 使用TypeScript時,不建議使用右鍵"Open in browser"功能,原因是Chutzpah每次執行會重新編譯TypeScript,產生類似_Chutzpah.53.sampleapp.js的暫存檔,為配合瀏覽器開啟檔案不會自動刪除,久而久之就會產生一大堆暫存檔,要改善這個問題,建議使用自訂TestRunner網頁取代。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

類別庫專案TypeScript不會自動編譯

$
0
0

進行Angular單元測試時,在Class Library(類別庫)新增TypeScript測試檔,使用Chutzpah測試正常,但我發現Class Library專案裡的TypeScript,不像在ASP.NET Web專案裡會自動產生.js檔,Chutzpah似乎是靠自己的機制編譯TypeScript(所以才會有名為_Chutzpah.53.sampleapp.js的暫存檔)。

計劃在Class Library加入自訂Test Runner網頁,需要將TypeScript轉為JavaScript才能運作。爬文找到參考文章,發現此與Class Library的csproj缺少設定有關。由於不同版本Visual Studio的參數有所出入(我實測的參數便與文章寫的不同),較保險的做法是拿可運作的ASP.NET專案與Class Library專案csproj比對,複製缺少的設定過去。

實際測試,Class Library的.csproj需增加兩個<Import>設定,剛好在開頭與結尾。

csproj開頭(紅框為要補上的設定)

csproj結尾

在csproj加入兩條<Import>設定並重新載入專案,之後每次儲存TypeScript,Visual Studio就會自動產生.js及.js.map囉~

Let It Go吧! System.Data.OracleClient

$
0
0

記得四年前微軟就正式宣告建議大家不要再用System.Data.OracleClient,改用ODP.NET。當時Oracle對LINQ支援還不太好,想用LINQ或EF得尋求3rd Party解決方案,從ODAC 11.2起,Entity Framework已是ODP.NET標準配備,System.Data.OracleClient的存在就更只剩下向前相容。

今天幫忙射掉茶包一枚: 使用SQLPlus執行SELECT * FROM BIGTABLE查詢約3萬6千筆資料耗時50秒,執行以下.NET程式卻耗時1分23秒,幾乎是一倍半的時間:

using System;
using System.Diagnostics;
using System.Data.OracleClient;
 
namespace ConsoleApplication1
{
class Program
    {
staticvoid Main(string[] args)
        {
string cnStr = "Data Source=Boo;User Id=User;Password=Password;";
 
            OracleConnection cn = new OracleConnection(cnStr);
            cn.Open();
 
            Stopwatch sw = new Stopwatch();
            sw.Start();
 
            var cmd = cn.CreateCommand();
            cmd.CommandText = "SELECT * FROM BIGTABLE";
            OracleDataReader dr = cmd.ExecuteReader();
while (dr.Read()) { }
            sw.Stop();
            Debug.WriteLine("Duration: {0:n0}", sw.ElapsedMilliseconds);
            Console.Read();
        }
    }
}

取得苦主提供的OracleCommand程式片段放進.NET 4 Console Application測試,我得到的結果卻大不相同! 在我機器上測試花不到50秒就跑完(跟用SQLPlus跑指令的時間相近),相同程式碼在兩台機器執行效果迥異令人費解? 仔細比對後發現: 苦主用了System.Data.OracleClient,而我想都沒想就拿出ODP.NET,換句話說,程式碼"幾乎"完全相同況,只有最上方using System.Data.OracleClient或是using Oracle.DataAccess.Client的差別,決定了不同命運,而且速度差異幅度高達50%!! 本以為System.Data.OracleClient只是官方不建議使用,萬萬沒想到續用會被懲罰,讓人意外,特發文補刀!

結論: 大家就比照IE6/7/8,讓System.Data.OracleClient早日回火星吧~ Let it go!

NG筆記5-下拉選單

$
0
0

復刻對象: KO範例2 - 下拉選單

在NG,下拉選單(<select>)跟<input>一樣用ng-model="..."建立雙向繫結,如要透過繫結動態產生選項,可用ng-options Directive。ng-options的語法較特別,需穿插as for in等關鍵字,寫法又有好幾種,保哥有篇詳解可參考。若以物件陣列為資料來源,有兩種做法:

1) 繫結到物件本身: "選項文字 for 物件變數 in 選項物件陣列"
2) 分別擊結文字及值: "選項值 as 選項文字 for 物件變數 in 選項物件陣列"

其中物件變數是ng-options就地新增的變數名稱,不需要事先定義在ViewModel中。若採取第一種做法,ng-model="model.result"繫結的對象會是物件,而非<option value="…">的value,因此下方要寫成<span ng-bind="model.result.t">才會顯示選項文字,而changeToPhone()中,也是透過指定物件改變選取結果: self.result = self.options[2] 。

按鈕事件用ng-click指定要執行的JavaScript函式,記得要加上括號,寫成ng-click="model.changeToPhone()"。

程式範例如下: Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 2 - 對應下拉選單</title>
</head>
<bodyng-controller="defaultCtrl">
<selectng-options="item.t for item in model.options"ng-model="model.result">
</select><br/>
<spanng-bind="model.result.v"></span><br/>
<inputtype="button"value="Set Phone"ng-click="model.changeToPhone()"/>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.options = [
                    { t: "PC", v: "PC" },
                    { t: "Notebook", v: "NB" },
                    { t: "Phone", v: "Phone" }
        ];
        self.result = self.options[0];
        self.changeToPhone = function() {
          self.result = self.options[2];
        }
      }
      $scope.model = new myViewModel();
    });
</script>
</body>
</html>

接著來看繫結選項文字及選項值的第二種做法: Live Demo

<bodyng-controller="defaultCtrl">
<selectng-options="item.v as item.t for item in model.options"ng-model="model.result">
</select><br/>
<spanng-bind="model.result"></span><br/>
<inputtype="button"value="Set Phone"ng-click="model.changeToPhone()"/>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.options = [
                    { t: "PC", v: "PC" },
                    { t: "Notebook", v: "NB" },
                    { t: "Phone", v: "Phone" }
        ];
        self.result = "NB";
        self.changeToPhone = function() {
          self.result = "Phone";
        }
      }
      $scope.model = new myViewModel();
    });
</script>
</body>

其中,ng-options被寫成"item.v as item.t for item in model.options",定義用item.v做為選項值,item.t做為選項文字。model.result的繫結對象也就變成了選項值,要指定result直接寫字串就好,例如: self.result = "Phone"。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記6-動態新增下拉選單選項

$
0
0

復刻對象: KO範例3 - 動態新增下拉選單選項

先示範一個失敗寫法。在KO範例裡,新增選項按鈕不包含在ViewModel範圍內,而是透過jQuery click事件在選項集合新增物件,而選項集合是ko.observabelArray(),KO能感測到新增動作,同步增加下拉選單選項;但同樣做法直接搬到NG行不通,option是尋常JavaScript陣列,NG感測不到Scope之外對ViewModel屬性的更動。如以下程式,按鈕後vm.options陣列雖已加入新元素,卻不會反應到下拉選單。Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 3 - 動態增加SELECT選項(無效)</title>
</head>
<bodyng-controller="defaultCtrl">
<selectid="selOptions"style="width: 120px"
ng-options="item.text for item in model.options"ng-model="model.result">
</select>
Result=<spanng-bind="model.result.value"></span>
 
<divstyle="margin-top: 10px">
Text: <inputid='txtOptText'value="Firefox"/>
Value: <inputid='txtOptValue'value="ff"/>&nbsp;
<inputtype="button"value="新增選項"id='btnAddOpt'/>
<divid="dvDebug"></div>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
var vm = null;
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.options = [
          { text: "IE", value: "ie" }
        ];
        self.result = self.options[0];
      }
      vm = new myViewModel();
      $scope.model = vm;
    });
    $("#btnAddOpt").click(function() {
      vm.options.push({
"text": $("#txtOptText").val(),
"value": $("#txtOptValue").val()          
      });
      $("#dvDebug").text(JSON.stringify(vm.options));
    });
</script>
</body>
</html>

在下圖中,options陣列已新增Firefox選項,但下拉選單卻仍只有一個選項,由dvDebug顯示的JSON.stringify(vm.options)可以驗證這點:

以上範例突顯了NG與KO的一項重要差異: KO需要明確宣告ko.observable()、ko.observableArray(),但不管任何時候變更這些受觀察物件都會引發UI及相依變數連動;而在NG中,一般的JavaScript物件屬性就可做為繫結對象,但相對地,要"在Scope感應範圍內更動資料,才會引發UI改變及連動"。就這個案例而言,將新增選項動作移入ng-click(),Scope便會將資枓變化反應到<select>。Live Demo 

<inputtype="button"value="新增選項"id='btnAddOpt'ng-click="model.addOption()"/>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
var vm = null;
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.options = [
          { text: "IE", value: "ie" }
        ];
        self.result = self.options[0];
        self.addOption = function() {
          self.options.push({
"text": $("#txtOptText").val(),
"value": $("#txtOptValue").val()
          });
        };
      }
      vm = new myViewModel();
      $scope.model = vm;
    });
</script>

(說明: 在ViewModel中存取$("#txt…")有違SoC原則,為不良設計。此處強調僅移動function()位置,故函式內部保留原樣)

想從NG事件之外更動ViewModel,需要一些技巧。NG提供jQuery.scope()方法,可以取得UI元素所屬的Scope物件,接著我們就可以存取到model物件及model.options: Live Demo

<script>
var vm = null;
    angular.module("sampleApp", [])
      .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
          self.options = [{
            text: "IE",
            value: "ie"
          }];
          self.result = self.options[0];
        }
        vm = new myViewModel();
        $scope.model = vm;
      });
    $("#btnAddOpt").click(function() {
var scope = $(this).scope();
      scope.model.options.push({
"text": $("#txtOptText").val(),
"value": $("#txtOptValue").val()
      });
      $("#dvDebug").text(JSON.stringify(scope.model.options));
    });
</script>

但是以上程式仍不管用,還差一個關鍵: 必須在Scope監視下更動資料,才會觸發繫結UI元素、連動變數或函數的更新。使用$scope.$apply()執行ViewModel更新,NG才能掌握資料異動。用法有三種: $scope.$apply("指令字串") 、 $scope.$apply(function() { 更新程式碼 }),或是依一般做法更新後再不帶參數呼叫$scopt.$apply()。Live Demo

    $("#btnAddOpt").click(function() {
var scope = $(this).scope();
//方法1: 傳入指令字串給$apply()執行
      scope.$apply("model.options.push({text:'Chrome',value:'chrome'})");
//方法2: 利用$apply()執行函式
      scope.$apply(function() {
        scope.model.options.push({
"text": $("#txtOptText").val(),
"value": $("#txtOptValue").val()
        });
      });
//方法3: 執行完畢呼叫$apply()[不帶參數]
      scope.model.options.push({text:"Safari",value:"safari"});
      scope.$apply();
      $("#dvDebug").text(JSON.stringify(scope.model.options));
    });
 
[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記7-以清單方式呈現資料

$
0
0

MVVM基本功,KO範例4 - 以清單方式呈現資料

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
 
<head>
<metacharset="utf-8">
<title>Lab 4 - 物件陣列繫結清單顯示</title>
<style>
        table { width: 400px }
        td,th { border: 1px solid gray; text-align: center }
        a.btn { text-decoration: underline; cursor: pointer; color: blue; }
</style>
</head>
<bodyng-controller="defaultCtrl">
<inputtype="button"value="新增User"ng-click="model.addUser()"/>
<span>{{ model.users.length }}</span>筆, 
合計 <span>{{ model.calcTotalScore() | number:0 }}</span>
<table>
<thead>
<tr>
<th>Id</th>
<th>姓名</th>
<th>積分</th>
<th></th>
</tr>
</thead>
<tbody>
<trng-repeat="user in model.users">
<td><span>{{ user.id }}</span>
</td>
<td><span>{{ user.name }}</span>
</td>
<td><spanstyle='text-align: right'>{{ user.score }}</span>
</td>
<td><ang-click="model.removeUser(user)"class="btn">移除</a>
</td>
</tr>
</tbody>
</table>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
 
//很簡單的User資料物件
function UserViewModel(id, name, score) {
var self = this;
        self.id = id;
        self.name = name;
        self.score = score;
      }
 
function myViewModel() {
var self = this;
        self.users = [];
        self.removeUser = function(user) {
          self.users.splice(self.users.indexOf(user), 1);
        };
var c = 3;
        self.addUser = function() {
var now = new Date(); //用時間產生隨機屬性值
          self.users.push(new UserViewModel(
"M" + c++,
"P" + "-" + now.getSeconds() * now.getMilliseconds(),
              now.getMilliseconds()));        
        };
        self.calcTotalScore = function() {
var sum = 0;
          $.each(self.users, function(i, user) {
            sum += user.score;
          });
return sum;
        };
      }
      vm = new myViewModel();
      vm.users.push(
new UserViewModel("M1", "Jeffrey", 32767));
      vm.users.push(
new UserViewModel("M2", "Darkthread", 65535));
      $scope.model = vm;
    });
</script>
</body>
 
</html>

Live Demo

在NG,ng-repeat="item in array"相當於KO的data-bind="foreach: array",但有兩點差異:

1) ng-repeat標註在重複出現的元素上(例如: <li>, <tr>),而KO foreach則寫在容器元素(例如: <ul>, <tbody>
2) ng-repeat用"item in array"為陣列元素指定臨時命名(即"item"),迴圈內用item.propName繫結

至於積分加總,寫一個計算函數跑迴圈就好,只要新增、刪除陣列元素動作寫在ng-click事件內,NG都能感應到變化自動重算。

繼續看KO範例5 - 即時反應物件屬性變化,如果陣列元素未增減,只是數值改變,NG也會重新加總嗎? 答案是: 會! 加總函數維持不變,加入改分數按鈕,單筆數字改變也會自動重算,請看Live Demo

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記8-初試自訂Directive

$
0
0

實做對象: KO範例6 - 陣列元素的新增/移除事件

Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
 
<head>
<metacharset="utf-8">
<title>KO範例6 - 陣列元素的新增/移除事件</title>
<style>
        table { width: 400px }
        td,th { border: 1px solid gray; text-align: center }
        a.btn { text-decoration: underline; cursor: pointer; color: blue; }
        tr.new { color: brown; }
</style>
</head>
<bodyng-controller="defaultCtrl">
<inputtype="button"value="新增User"ng-click="model.addUser()"/>
<span>{{ model.users.length }}</span>筆, 
合計 <span>{{ model.calcTotalScore() | number:0 }}</span>
<table>
<thead>
<tr>
<th>Id</th>
<th>姓名</th>
<th>積分</th>
<th></th>
</tr>
</thead>
<tbody>
<trng-repeat="user in model.users"ng-class="user.addFlag ? 'new' : ''"
anim-hide="user.removeFlag"anim-hide-done="model.removeUser()">
<td><span>{{ user.id }}</span>
</td>
<td><span>{{ user.name }}</span>
</td>
<td><spanstyle='text-align: right'>{{ user.score }}</span>
</td>
<td><ang-click="model.markUserRemoved(user)"class="btn">移除</a>
</td>
</tr>
</tbody>
</table>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .directive("animHide", function() {
returnfunction(scope, element, attrs) {
        scope.$watch(attrs["animHide"], function(newValue, oldValue) {
if (newValue == true) {
            element.css("background-color", "red")
            .animate({ opacity: 0.2 }, 500, function () {
              scope.$parent.$apply(attrs["animHideDone"]);
            })
 
          }
        });
      };
    })
    .controller("defaultCtrl", function($scope) {
 
//很簡單的User資料物件
function UserViewModel(id, name, score) {
var self = this;
        self.id = id;
        self.name = name;
        self.score = score;
        self.removeFlag = false;
        self.addFlag = false;
      }
 
function myViewModel() {
var self = this;
        self.users = [];
        self.userToRemove = null;
        self.markUserRemoved = function(user) {
          user.removeFlag = true;
          self.userToRemove = user;
        };
        self.removeUser = function() {
          self.users.splice(self.users.indexOf(self.userToRemove), 1);
        }
var c = 3;
        self.addUser = function() {
var now = new Date(); //用時間產生隨機屬性值
var user = new UserViewModel(
"M" + c++,
"P" + "-" + now.getSeconds() * now.getMilliseconds(),
              now.getMilliseconds());
//將現有user.addFlag清掉
          $.each(self.users, function(i, u) { u.addFlag = false; });
          user.addFlag = true;
          self.users.push(user);
        };
        self.calcTotalScore = function() {
var sum = 0;
          $.each(self.users, function(i, user) {
            sum += user.score;
          });
return sum;
        };
      }
      vm = new myViewModel();
      vm.users.push(
new UserViewModel("M1", "Jeffrey", 32767));
      vm.users.push(
new UserViewModel("M2", "Darkthread", 65535));
      $scope.model = vm;
    });
</script>
</body>
 
</html>

NG沒有ko.observableArray()物件,不像有KO有afterAdd/beforeRemove事件可用,為了實現跟KO範例6一樣的效果,使用以下技巧:

  • 新增資料時,將最新加入的資料標為暗紅字
    ViewModel增加addFlag屬性,新增User ng-click事件將現存資料的addFlag全設成false,只留新資料為true。再透過ng-class="user.addFlag ? 'new' : ''"讓addFlag==true的<tr>套用暗紅樣式。
  • 刪除資料時,加上資料淡出後消失的特效
    由於要在淡出動畫結束後再刪除資料,我想到最直覺的做法是透過jQuery.animate()遞減opacity並在動畫結束事件刪除資料,但ViewModel不該涉及UI,決定寫下第一個自訂Directive當練習。

透過module().directive()可以加入自訂的Directive,我們設計一個animHide Directive,藉由偵測指定屬性變化,在屬性值變成true時觸發jQuery.hide()動畫效果,並在動畫結束後呼叫指定函式(執行刪除動作)。把這段程式抽出來單獨看:

<script>
    angular.module("sampleApp", [])
    .directive("animHide", function() {
returnfunction(scope, element, attrs) {
        scope.$watch(attrs["animHide"], function(newValue, oldValue) {
if (newValue == true) {
            element.css("background-color", "red")
            .animate({ opacity: 0.2 }, 500, function () {
              scope.$parent.$apply(attrs["animHideDone"]);
            })
          }
        });
      };
    })
    .controller("defaultCtrl", function($scope) {
        //...略...
    });
</script>

.directive("animHide", function(){ ... })的第二個函式參數會在建立Directive時呼叫,需傳回Directive設定物件,針對較簡單的Directive,只需指定設定物件中的link函式屬性。原本應該傳回return { scope: …, restrict: …, link: function() { … }, … },寫成return funtion(scope, element, attrs) { ... },NG就視為只指定link函式,其他設定使用預設值。

link函式會在Directive套用到UI元素時執行(可以想成初始化期間要完成的工作),在函式中可透過scope存取當下的ViewModel物件,用element存取UI元素的jQuery物件,attrs則讓Directive經由HTML Attribute讀取額外參數。在這個案例中,我們不把動畫結束後的事寫死在程式裡,而是用animHideDone="刪除動作"指定(注意: 在HTML寫成anim-hide-done="",讀取時寫成Camel格式: attrs["animHideDone"]),這樣比較彈性。補充一點: 在Angular的設計中,ViewModel不應涉及UI,以貫徹SoC棈神並利於單元測試,Directive是放置ViewModel與HTML元素互動邏輯的最佳場合,

在link函式裡,我們第一件要做的事是要求NG注意某個屬性值的變化,一旦改變時要通知我們。scope.$watch()是NG用來建立連動關係的重要方法,類似KO的ko.computed(),第一個參數傳入表示式字串或函式,NG評估表示式字串或函式傳回結果,一旦其關聯屬性出現變化,便會觸發第二個參數指定的事件,事件可取得關聯屬性的新、舊值(newValue及oldValue),依新舊值決定如何因應。在我們的Diretive中,一旦newValue為true時,透過jQuery先將element背景改為紅色,再執行顏色刷淡(opacity由1降為0.2)動畫效果,結束後執行animHideDone所指定的刪除動作,為了確保NG感測到刪除元素的變化,記得要用scope.$apply()執行。

定義好animHide Directive,我們在<tr>加上宣告: <tr ng-repeat="user in model.users" ng-class="user.addFlag ? 'new' : ''" anim-hide="user.removeFlag" anim-hide-done="model.removeUser()">,背後會$watch("user.removeFlag", …),一旦user.removeFlag為true,就引發背景色變紅並淡出,動畫結束時執行model.removeUser()將User從陣列移除。

如此,要刪除資料時,將removeFlag設為true,就會先播動畫再移除資料,符合規格要求。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

【茶包射手日記】OracleDataReader數字型別對應陷阱(ODP.NET)

$
0
0

接獲報案,由Oracle資料庫讀取NUMBER(14,6)欄位,原值為1.997748,JSON轉換送至Client端變成1.9977479999999999!

判斷這是典型浮點數問題(順便複習: 算錢用浮點,遲早被人扁),但檢視程式,OracleCommand執行查詢,由NUMBER(14,6)欄位直接SELECT取值,未經過任何計算,資料由OracleDataReader讀取交給JsonConvert轉換,程式寫法沒有可議之處,決定使用浮點型別(double)的犯人--顯然是OracleDataReader! 試著CAST將該欄位轉型成NUMBER(20,6),OracleDataReader便會視為decimal,不再有浮點誤差問題。

用一小段程式重現問題: (.NET 4 Console Appliation,使用 Oracle.DataAccess.Client 2.112.3/4.112.3/4.121.1 測試結果相同)

staticvoid Main(string[] args)
        {
 
            OracleConnection cn = new OracleConnection(strConnString);
            cn.Open();
 
            var cmd = cn.CreateCommand();
            cmd.CommandText = @"select 
cast(1.997748 as number(14,6)) as n1,
cast(1.997748 as number(20,6)) as n2
from dual";
            OracleDataReader dr = cmd.ExecuteReader();
 
            dr.Read();
            Console.WriteLine("ToString()\t{0}\t{1}", 
                dr["n1"].ToString(), dr["n2"].ToString());
            Console.WriteLine("JSON\t{0}\t{1}",
                JsonConvert.SerializeObject(dr["n1"]),
                JsonConvert.SerializeObject(dr["n2"]));
            Console.WriteLine("DataType\t{0}\t{1}",
                dr.GetFieldType(0), dr.GetFieldType(1));
            Console.Read();
        }

執行結果如下:

ToString()      1.997748        1.997748
JSON    1.9977479999999999      1.997748
DataType        System.Double   System.Decimal

由此驗證,同樣的數字,對於NUMBER(14,6)、NUMBER(20,6)兩種型別,OracleDataReader會選用System.Double或System.Decimal承接。另外還有一項有趣發現 -- double 1.9977479999999999的ToString()結果是"1.997748",害我一開始被假象唬得團團轉。

查了Oracle官方文件,上面是這麼說的:

Certain methods and properties of the OracleDataReader object require ODP.NET to map a NUMBER column to a .NET type based on the precision and scale of the column. These members are:

Item property, GetFieldType method, GetValue method, GetValues method

ODP.NET determines the appropriate .NET type by considering the following .NET types in order, and selecting the **first** .NET type from the list that can represent the entire range of values of the column:

System.Byte, System.Int16, System.Int32, System.Int64, System.Single, System.Double, System.Decimal

If no .NET type exists that can represent the entire range of values of the column, then an attempt is made to represent the column values as a System.Decimal type. If the value in the column cannot be represented as System.Decimal, then an exception is raised.

For example, consider two columns defined as NUMBER(4,0) and NUMBER(10,2). The first .NET types from the previous list that can represent the entire range of values of the columns are System.Int16 and System.Double, respectively. However, consider a column defined as NUMBER(20,10). In this case, there is no .NET type that can represent the entire range of values on the column, so an attempt is made to return values in the column as a System.Decimal type. If a value in the column cannot be represented as a System.Decimal type, then an exception is raised.

依此推論,呼叫GetValue()、GetFieldType()讀取NUMBER型別欄位,ODP.NET會依byte, short, long, single, double, decimal的順序,使用第一個能完整容納精確數字的型別。

寫一小段程式驗證:

staticvoid Main(string[] args)
        {
            OracleConnection cn = new OracleConnection(strConnString);
            cn.Open();
            var cmd = cn.CreateCommand();
            cmd.CommandText = @"select 
cast(1 as number(1,0)) as n1,
cast(12 as number(5,0)) as n2,
cast(12 as number(5,1)) as n3,
cast(12 as number(10,0)) as n4,
cast(12 as number(10,1)) as n5,
cast(12 as number(15,0)) as n6,
cast(12 as number(15,1)) as n7,
cast(12 as number(20,0)) as n8,
cast(12 as number(20,1)) as n9
from dual";
            OracleDataReader dr = cmd.ExecuteReader();
 
            dr.Read();
for (int i = 0; i < 9; i++)
            {
                Console.WriteLine("T:{0} V:{1}",
                    dr.GetFieldType(i), dr.GetValue(i));
            }
            Console.Read();
        }

隨著小數位數增加,OracleDataReader陸續選用Int16, Int32, Int64, Single, Double, Decimal(但試不出文件說的Byte)

T:System.Int16 V:1
T:System.Int32 V:12
T:System.Single V:12
T:System.Int64 V:12
T:System.Double V:12
T:System.Int64 V:12
T:System.Double V:12
T:System.Decimal V:12
T:System.Decimal V:12

OracleDataReader確實會依數字精確度決定型別,但官方文件並未說明規則(或許也意味著隨時可能修正,應該視為黑盒子),熬不過好奇心驅使,我還是反組譯OracleDataAccess把演算法挖了出來: (以4.112.3版本為例)

假設數字精準度為NUMBER(Precision, Scale),不考慮Scale < 0的罕見情境

  • Scale = 0 && Precision < 5 –> short
  • Scale = 0 && Precision < 10 –> int
  • Scale = 0 && Precision < 19 –> long
  • Precision < 8 && 0 < Scale < 50 –> float
  • Scale > 0 &&  Precision < 16 –> double
  • 其他 –> decimal

回到NUMBER(14,6)被轉成double的問題上。自動判斷型別發生於GetValue()、ToString(),OracleDataReader在底層仍明確知道真實型別,用dr.GetProviderSpecificFieldType(0)可取得Oracle.DataAccess.Types.OracleDecimal型別,而dr.GetDecimal(0)也能正確讀出1.997748。但大部分通用型的轉換程式,多半只用DataReader.GetValue()直接取回object型別,若加入GetProviderSpecificFieldType()邏輯將會對資料庫種別產生依賴。

找不到改變ODP.NET OracleDataReader自勳判斷行為的方法(OracleDataAdapter有所謂Safe Type Mapping,但不能直接用於OracleDataReader),要避免NUMBER()被轉成浮數,我想到以下幾種做法:

  1. SELECT時用CAST轉型為16位以上的精確度,例如: NUBMER(16,n),強迫OracleDataReader以decimal處理
  2. 改用Entity Framework等ORM做法
  3. 資料讀取程式針對ODP.NET加入邏輯,以GetProviderSpecificFieldType()偵測OracleDecimal,一律用.GetDecimal()取值傳回decimal

PS: System.Data.OracleClient有來自火星的外電消息: 我的OracleDataReader遇到NUMBER一律傳回decimal...

TypeScript的函式多載(Overloading)

$
0
0

考慮以下JavaScript函式(myText):

/// <reference path="../Scripts/jquery-2.1.1.js" />
 
function myText(selector, valueOrFunction) {
//未指定第二個參數時,傳回jQuery .text()
if (valueOrFunction === undefined) {
return $(selector).text();
    }
//第二個參數可以是數值或函式,如為函式則呼叫取值
var value = $.isFunction(valueOrFunction) ?
                valueOrFunction() : valueOrFunction;
    $(selector).text(value);
}
 
 
$(function () {
var src = myText("#src")
    $("#t1").text(src);
    myText("#t2", "Given by Value");
    myText("#t3", function () {
return"Given by Function: " + parseInt(Math.random() * 10000);
    });
});

myText的傳入參數selector及valueOrFunction具有彈性: 當valueOrFunction未提供時會傳回jQuery(selector).text();valueOrFunction有值時使用jQuery(selector).text(...)設定元素的文字內容,valueOrFunction可以是字串或函式,如果是函式則由呼叫函式動態取得字串。

若以C#觀點,myText()方法有三個多載(Overloading),分別是myText(string selector)、myText(string selector, string value)及myText(string selector, Func<stirng> func)。 在TypeScript,我們也能實現類似的函式Overloading效果,將以上的函式改寫成TypeScript: (註: 嚴格來說,TypeScript多載與C#並不完全相同,C#多載的傳回型別必須一致)

/// <reference path="../scripts/typings/jquery/jquery.d.ts" />
 
/** 
    * 取得jQuery .text() 
    * @param selector jQuery選擇器字串
    */
function myText(selector: string): string;
/** 
    * 使用jQuery .text()設定文字內容
    * @param selector jQuery選擇器字串
    * @param value 要設定的字串內容
    */
function myText(selector: string, value: string):void;
/** 
    * 使用jQuery .text()設定文字內容
    * @param selector jQuery選擇器字串
    * @param func 函式,傳回待設定內容
    */
function myText(selector: string, func: () => string): void;
function myText(selector: string, valueOrFunction?: any) {
if (valueOrFunction === undefined) {
return jQuery(selector).text();
    }
var value: string = jQuery.isFunction(valueOrFunction) ?
        valueOrFunction() : valueOrFunction;
    jQuery(selector).text(value);
}
 
jQuery(function () {
var src = myText("#src")
    $("#t1").text(src);
    myText("#t2", "Given by Value");
    myText("#t3", function () {
return"Given by Function: " + (Math.random() * 10000).toFixed(0);
    });
});

我們宣告三個沒有內容的函數宣告:

function myText(selector: string): string;
function myText(selector: string, value: string):void;
function myText(selector: string, func: () => string): void;

其中第三個用Lambda語法指定func參數型別需為一個傳回字串的函式。最後的function myText(selector: string, valueOrFunction?: any)提供真正的實做,由於valueOrFunction參數可有可無,要寫為"valueOrFunction?",否則function myText(selector: string)會無法對應。

以上的TypeScript會轉換成如下的BLOCKED SCRIPT

/// <reference path="../scripts/typings/jquery/jquery.d.ts" />
 
 
function myText(selector, valueOrFunction) {
if (valueOrFunction === undefined) {
return jQuery(selector).text();
    }
var value = jQuery.isFunction(valueOrFunction) ? valueOrFunction() : valueOrFunction;
    jQuery(selector).text(value);
}
 
jQuery(function () {
var src = myText("#src");
    $("#t1").text(src);
    myText("#t2", "Given by Value");
    myText("#t3", function () {
return"Given by Function: " + (Math.random() * 10000).toFixed(0);
    });
});
//# sourceMappingURL=typescript.js.map

最前方三個額外函式宣告不會在JavaScript中出現,但會在編輯及編譯階段發揮Strong Type的威力:

首先,打入myText(時,Intellisense會像C#一樣列舉三個多載選擇,透過JSDoc,函式及參數都有中文說明。

故意寫了四行不符型別的myText()呼叫方式,通通被打槍標上紅蚯蚓:

  • myText(2) <== selector不能是number
  • var i: number = myText("#t1") <== 傳回值必須用字串變數承接
  • myText("#t1", 5) <== 沒有第二個參數為number的overloading
    注意: 宣告多載時,實做函式myText(selector: string, valueOrFunction?: any)不納入多載選項。
  • myText("#t1", () => { return 6; }); <== 第二參數雖然是函式,但傳回值不是string而是number
    註: () => { return 6; } 相當於 function() { return 6; },在TypeScript可以用Lambda寫匿名函式。

很嚴謹很機車吧? 愈機車愈是深得我心,不愧為錯字天王的救星~

2014石碇馬拉松~

$
0
0

第16馬,石碇馬拉松。

馬拉松是一種運動,夏天的馬拉松是一種"極限運動",在夏天跑山路馬是一種"燃燒生命的極限運動"... Orz

當石碇馬拉松的簡章出來,明知是盛夏,明知有爬不完的山路,衝著石碇離家不遠,又是七小時關門,手一滑就報了名。老早報好名,誰知前一場海山馬拐馬腳以落馬收場,高掛跑鞋近月養傷,前一週還在棄賽與否間猶豫。直到賽前幾天小心翼翼實測,連三天試跑5K,8K及政大環山道,能無痛跑到6分速,並與領隊、教練及隊醫(哪裡冒出來的角色?)商討,決定復行視事,體壇無不雀躍… (謎: 這位先生,你有事嗎?)

跑是能跑,不在最佳狀況,不能胡來,加上高溫的嚴峻考驗,擬訂好"只講求不傷身體,置成績於度外"的終極目標,反正這輩子沒跑超過六小時的馬拉松,就安步當俥當超馬跑吧!

用這張搏命畫成的高度表揭開序幕... (海拔100米->570米->310米->560米->450米->560米->450米->560米->310米->570米->100米,共爬升470+250+110+110+260=1,300米!!)

清晨五點半,會場人聲鼎沸,如果沒認錯,主持人應是超馬神人鄒雙喜上校吧! 今天有幾位前輩要完成百馬及兩百馬。

全馬只有八百多位選手參加,是場小而美的賽事,遠方的高架是國道五號。

起跑前大會發了個含蓋的小水杯,今天沿路水站不提供紙杯,少砍幾棵樹也少製造一點垃圾。

一起跑就是爬不完的上坡,遠處山景讓我憶起過往假日會跑來深坑石碇平溪四處攻山頭的歲月,令人懷念~

走著S形小徑穿過茶樹間蜿蜒上山,應該要很浪漫,但日頭這麼大我只覺得很"暗"。

酷暑當頭,雖然報名這場本身就是失心瘋的行為,但跑友們今天已完全恢復理智,知道此刻用走的才是王道! (我全程盯著心跳表,上坡光用走的心率都可能破95%,很可怕! 只好放慢再放慢)

好不容易來到第二折返點華梵大學,心想快到了快到了,但一路一直往校園最高點走個沒完是怎樣?

嗯,還真的爬到校園高點才回頭,好夠本呀! (流淚...)

      

路上有間小工廠,院子裡推滿了各式各樣的玻璃纖維雕像的模具,原來雕像是這麼做出來的!

最後10K,跑者間的距離開始拖得很長,有時甚至前後都不見其他跑友,甚至會擔心自己是不是跑錯路。

上坡用走的,下坡也不想跑,耗時6:36:51,終算回到可愛的終點,有跑友已在溪中做起SPA~ 距離略遠看不出是否有用AngularJS。(謎: 先生,你中毒也太深了吧?)

會場架了帳篷圍出盥洗室,乾淨又寬敞,到後段班都還有水可用(這是小而美賽事的好處吧!),第一次在會場沖澡,很感動。沖洗換裝完成,完成百馬的前輩正在接受頒獎,上面照片的另一個重點是最上方的半截溫度計 -- 37度! orz…

     

公版獎牌,但背後印了跑者姓名,算是小而美賽事另一項專屬待遇。

搭公車回到木柵,決定騎U-Bike回家,起步沒多久便大雨滂沱,淋了一身濕。今天又跑又騎又泡水,能勉強算是鐵人三項嗎? XD

VS2012網站專案的TypeScript不會自動編譯

$
0
0

接獲報案,在Visual Studio 2012若建立HTML Application with TypeScript專案(如下圖),.ts可順利產生.js及.js.map,運作正常。

但是若建立的是一般ASP.NET網站專案(Visual C#/Web下的項目),可以新增及編輯.ts,編譯時卻不會產生.js。

想起上回處理TypeScript in Class Library的經驗,猜想又是csproj短少設定造成。比對兩種專案的csproj,在檔案尾端發現以下可疑設定,HTML Application with TypeScript專案有,一般網站專案沒有:

</ProjectExtensions>
<PropertyGroupCondition="'$(Configuration)' == 'Debug'">
<TypeScriptTarget>ES5</TypeScriptTarget>
<TypeScriptRemoveComments>false</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
<TypeScriptModuleKind>AMD</TypeScriptModuleKind>
</PropertyGroup>
<PropertyGroupCondition="'$(Configuration)' == 'Release'">
<TypeScriptTarget>ES5</TypeScriptTarget>
<TypeScriptRemoveComments>true</TypeScriptRemoveComments>
<TypeScriptSourceMap>false</TypeScriptSourceMap>
<TypeScriptModuleKind>AMD</TypeScriptModuleKind>
</PropertyGroup>
<ImportProject="$(VSToolsPath)\TypeScript\Microsoft.TypeScript.targets"
Condition="Exists('$(VSToolsPath)\TypeScript\Microsoft.TypeScript.targets')"/>
</Project>

兩個<PropertyGroup>用來控制Debug及Release模式使用不同編譯參數,實測只需補上最後一則<Import>到一般網站的csproj檔,就可解決TypeScript不會編譯的問題。

事後用關鍵字"Microsoft.TypeScript.targets"搜索,在TypeScript wiki找到說明,stackoverflow上諸多討論也提及補設定做法,算是VS2012的專案範本對TypeScript支援度不足所致,未來經過更新應會改善。

NG筆記9-visible, disable, css與屬性連動

$
0
0

題目: 使用NG復刻KO範例7 - visible, disable, css繫結

Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 7 - visible, disable, css與屬性連動</title>
<style>
    .urgent
    {
        background-color: #ffcccc;
        border: 1px solid red;
    }
</style>
</head>
<bodyng-controller="defaultCtrl">
<inputtype="checkbox"ng-model="done"/>
<inputtype="text"style="width: 200px;"ng-disabled="done"
ng-model="task"ng-class="task.indexOf('急') > -1 ? 'urgent' : ''" />
<spanng-show="done">完成!</span>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.task = "";
        self.done = false;
      }
      $scope.model = new myViewModel();
    });
</script>
</body>
</html>

很簡單的範例,沒太多學問,用到以下三個Directive: ng-disabled, ng-class, ng-show(用ng-hide亦可),打完收工~

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記10 - 保安,可以讓Angular這樣算了又算嗎?

$
0
0

網友kcw問了一個好問題,提到計算型屬性函數出現會重覆執行兩次的現象!

一句話點醒我夢中人,嚇得我屁滾尿流失了魂~~~

花了點時間研究,才發現原來我學藝不精,一直沒搞通Angular的屬性相依運作原理,時常誤用KO概念思考。謝謝kcw的問題,讓我釐清一塊暗藏地雷的危險地帶。(註: 相依性追蹤往往是MVVM最複雜的一部分,稍有不慎就可能中箭還不知敵在何方,KO亦然)

先看一個Demo,我改寫NG筆記2-網頁MVVM基本架構的計算型屬性範例,拿掉{{ fullName() }},只留<span ng-bind="fullName()">,並在ViewModel的function fullName()中加入計數器:

function myViewModel() {
var self = this;
        self.firstName="Jeffrey";
        self.lastName="Lee";
var c = 1;
        self.fullName = function() {
          window.console && console.log(
"fullName() calculated(" + (c++) + "): " + 
             self.firstName + " " + self.lastName);
return self.firstName + " " + self.lastName;
        };
      }

如圖所示,一開始fullName()執行了兩次,內容是初始值"Jeffrey Lee",而之後每輸入一個字元,都會呼叫fullName()兩次。如kcw所提的,每次更動就會觸發兩次重算!

要解釋這個現象,要從Angular相依性追蹤機制說起。NG很為人稱道的一點,是它能監測一般JavaScript元件及屬性變化,省去KO需明確宣告ko.observable()、ko.observableArray()的麻煩,其中魔法來自$watch()$digest()。當使用$watch()或透過ng-model、ng-bind建立繫結時,NG會建立一個Watcher,當每次呼叫$digest()時,所有的Watcher便會重新讀取或計算取值,若觀察對象的值發生變化,NG便會呼叫Watcher註冊的Listener( 即function(newValue, oldValue) { … }函式,可參考筆記8的animHide Directive )執行指定作業。由於Listener也可能更動$scope屬性,進一步連動其他Watcher及Listener,原Watcher可能需要再次重算以反應其他Listener的更新。為了滿足這種情境,NG的解決方案是再跑一次$digest()重算所有Watcher的觀察對象,直到所有計算值不再改變為止。當然,當出現循環參照時(A屬性改變影響B屬性,B的改變又會影響A),會產生無窮迴圈,所以NG訂了一個上限,最多重跑10次,若一直無法穩定就放棄。(原文: The watch listener may change the model, which may trigger other listeners to fire. This is achieved by rerunning the watchers until no changes are detected. The rerun iteration limit is 10 to prevent an infinite loop deadlock.)

如此就能解釋fullName()會跑兩次的原因,ng-bind="model.fullName()"建立了Watcher,當firstName或lastName變動時,Watcher觸發fullName()重算,得到結果後NG再跑一次$digest()以確定所有Watcher的值都不再變化,$digest()執行了ng-bind="model.fullName()" Watcher,於是fullName()跑了兩次。

接著做第二個實驗,除了ng-bind="model.fullName()",再多加一個{{ model.fullName() }},這樣就有兩個Watcher,猜猜會跑幾次? 答案是四次!

更狠一點,我們把計數器也變成ViewModel的屬性之一,並且用<span>{{model.c}}</span>顯示在網頁上(這代表會有增加一個Watcher觀察Counter變化),此時會發生什麼狀況? 是的,循環參考!model.fullName()改變了model.c,呼叫$digest()檢查時再次呼叫fullName()時再次改變model.c,$digest()蒐集的結果與前次不同,NG判斷需再重跑$digest(),fullName()再次執行,model.c又變了,$digest()結果不吻合又要重跑$digest()… 就這麼沒完沒了,最後突破10次重算上限被NG強制中止!

如果不是因為循環參照而是屬性一直在改變呢? 我們故意惡搞讓model.fullName()傳回結果包含亂數:

        self.fullName = function() {
          window.console && console.log(
"fullName() calculated(" + (c++) + "): " + 
            self.firstName + " " + self.lastName);
return self.firstName + " " + self.lastName + " " +
            Math.random();
        };

薑!薑!薑!薑~~ 一樣會因為重算超過10數爆掉!

再用一個實驗觀察NG重算的順序,在ViewModel中新增fullName$()及fullName$$()。

function myViewModel() {
var self = this;
        self.firstName="Jeffrey";
        self.lastName="Lee";
var c = 0;
        self.fullName = function() {
var fn = self.firstName + " " + self.lastName;
          window.console && console.log(
"fullName() calculated(" + (c++) + "): " + fn);
return fn;
        };
        self.fullName$ = function() {
var fn = self.fullName() + "$";
          window.console && console.log(
"fullName$() calculated(" + (c++) + "): " + fn);
return fn;
        };
        self.fullName$$ = function() {
var fn = self.fullName$() + "$";
          window.console && console.log(
"fullName$$() calculated(" + (c++) + "): " + fn);
return fn;
        };
      }

由Console輸出結果,推測NG的重算執行順序如下:

  1. ng-bind="model.fullName()" Watcher初始化觸發fullName()計算,對應到calculated(0)
  2. ng-bind="model.fullName$()" Watcher初始化觸發fullName$(),其中有var fn = self.fullName() + "$"; 呼叫fullName(),故出現fullName() calculated(1),fullName$() calculated(2)
  3. ng-bind="model.fullName$$()" Watcher初始化觸發fullName$$(),其中有var fn = self.fullName$(),self.fullName$()又呼叫了self.fullName(),故順序為fullName() calculated(3),fullName$() calculated(4),fullName$$() calculated(5)
  4. 所有Watcher執行完畢,NG要Rerun以確定所有值都不再變動,故1,2,3三個Watcher會再重新取一次值,故用同樣的順序重新執行calculated(6)到calculated(11)

調整<div ng-bind="…">順序:

<divng-bind="model.fullName$$()"></div>
<divng-bind="model.fullName$()"></div>
<divng-bind="model.fullName()"></div>

結果變成: fullName(), fullName$(), fullName$$(), fullName(), fullName$(), fullName(),驗證重算順序與ng-bind出現順序(亦等同Watcher註冊順序)有關。

最後,我們來觀察$digest()、$apply()、$eval()如何觸發重算?

在網頁新增六顆鈕,用jQuery click()事件分別執行$eval()、$eval("model.firstName")、$eval("model.fullName()")、$digest()、$apply(),第六顆鈕則加入ng-click事件但除了寫Log什麼都不做:

<body>
<divng-controller="mainCtrl">
<inputtype="text"ng-model="model.firstName"/>
<inputtype="text"ng-model="model.lastName"/>
<divng-bind="model.fullName()"></div>
<div>
<inputtype="button"value="$eval()"data-action="runEval"/>
<inputtype="button"value="$eval()-firstName"data-action="runEval,model.firstName"/>
<inputtype="button"value="$eval()-fullName"data-action="runEval,model.fullName()"/>
<inputtype="button"value="$digest"data-action="runDigest"/>
<inputtype="button"value="$apply"data-action="runApply"/>
<inputtype="button"value="ng-click"ng-click="doNothing()"
&lt;/div>
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
function log(m) {
      window.console && console.log(m);
    }
var scope = null;
    angular.module("sampleApp", [])
    .controller("mainCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.firstName="Jeffrey";
        self.lastName="Lee";
var c = 0;
        self.fullName = function() {
var fn = self.firstName + " " + self.lastName;
          log("fullName() calculated(" + (c++) + "): " + fn);
return fn;
        };
      }
      $scope.doNothing = function() { 
        log("ng-clicked");
      };
      $scope.model = new myViewModel();
      scope = $scope;
    });
    $(":button").click(function() {
var action = $(this).data("action");
if (!action) return; //for ng-click button
if (action == "runDigest") {
        log("Run $digest");
        scope.$digest();
      }
elseif (action == "runApply") {
        log("Run $apply");
        scope.$apply();
      }
elseif (action == "runEval") {
        log("Run $eval()");
        scope.$eval();
      }
else {
        expr = action.split(',')[1];
        log("Run $eval(" + expr + ")");
        scope.$eval(expr);
      }
    });
</script>
</body>

實際測試,各指令引發的fullName()計算次數如下:

  • $eval() 0次
  • $eval("model.firstName") 0次
  • $eval("model.fullName()") 1次
  • $digest() 1次
  • $apply() 1次
  • ng-click 1次

由結果可知,$digest()會重算一次,$eval()只有在涉及fullName()時才會觸發重算,$apply()等於$eval() + $digest()[請參見文件Pseudo-Code of $apply()一節],故重算次為1,有趣的是ng-click事件會被包在$apply()中,所以也會觸發一次重算。

由以上觀察,我學到一件事:

Angular不需宣告特殊Observable物件就能實現屬性相依重算,但背後需藉由反覆重跑$digest()以確定資料停止變動。每次資料異動後,相依的計算型屬性會連續執行兩次以上(通常是兩次,但最多可能到10次),這是Angular的設計機制使然。了解此一特性後,開發時需留意反覆執行的運算成本,避免在計算型屬性放入複雜運算邏輯(尤其要避免AJAX呼叫),否則將有損效能。

了解這點後,我開始懷念Knockout的ko.observable()、ko.computed(),雖然宣告時麻煩,但相依性的連動比Angular靠反覆檢查來得直覺有效,只能說每種做法各有優劣,每個決策背後都意味取捨,既然決定採用,就要熟悉你的工具的長處與短處,才能發揮最大的威力。

為jQuery Plugin撰寫TypeScript定義檔

$
0
0

TypeScript是強型別的世界,透過預先宣告物件、屬性、方法、介面,在編輯階段提供Intellisense提示、Go Definition、Find All References、Rename... 等編譯式語言才有的功能,而編譯時可預先抓出參數、型別、方法錯誤,降低執行階段發現修復的高昂成本。

開始使用TypeScript一段時間後,一定會發現一項困擾: 雖然DefinitelyTyped計劃已匯集許多常用JavaScript程式庫的TypeScript定義檔,但畢竟無法涵蓋你會用到的每一個JavaScript程式庫,以我自己為例,馬上面對的問題的便是: 我找不到jQuery BlockUI的定義檔。

於是,在程式中要引用block(),blockUI()會出現紅蚯蚓無法編譯。

有一個取巧解法,只要不在全域範圍,在module、interface、class內部可以透過declare指令重新將jQuery、$定義成any型別,declare的變數並不會出現在JavaScript中,純粹讓編譯器假設該變數存在。如此jQuery變成任意型別物件,不管加上什麼屬性、呼叫任何方法都視為合法,但這得付出代價 – 與jQuery相關的Intellisense與編輯檢查從此失效,退回JavaScript時代。

我曾想到一個投機做法,declare var $就好,當需要強型別時寫jQuery("div"),需要用無定義檔方法時寫$("div")。不過身為程式魔人,一直偷雞摸狗下去也不是辦法,還是乖乖學會怎麼為jQuery Plugin寫定義檔吧!

這裡先試寫一個簡單但無聊的jQuery Plugin當成練習目標,為了涵蓋常遇到的各式情境,我刻意加入多種存取API:

$("…").fill(); 元素塗色(用預設顏色)
$("...").fill({ color: "red" }); 元素塗色(指定顏色)
$.fill(); 網頁塗色(用預設顏色)
$.fill({ color: "red" }); 網頁塗色(指定顏色)
$.fill.options.color = "blue"; 修改預設顏色
$.title(); 取得網頁標題
$.title("…"); 設定網頁標題

TypeScript程式碼如下:

/** jquery.fill options */
interface JQFillOptions {
/** fill color */
    color?: string;
}
 
(function ($) {
var defaultOptions: JQFillOptions = {
        color: "red"
    };
//merge option and default option to get fill color
function getColor(options?: JQFillOptions) {
return $.extend({}, defaultOptions, options).color;
    }
//fill background to document.body
    $.fill = function (options?: JQFillOptions) {
        $("body").css("background-color", getColor(options));
    }
//global options
    $.fill.options = defaultOptions;
//fill background for element
    $.fn.fill = function (options?: JQFillOptions) {
returnthis.each(function () {
            $(this).css("background-color", getColor(options));
        });
    };
//get and set document.title
    $.title = function (title?: string) {
if (title)
            document.title = title;
else
return document.title;
    };
})(jQuery);

寫個網頁測試:

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>jQuery Fill Plugin</title>
<style>
        div { 
            margin: 12px; padding: 6px; width: 100px; 
            color: white;
        }
</style>
</head>
<body>
<div>Test</div>
<div>Test</div>
<scriptsrc="../Scripts/jquery-2.1.1.js"></script>
<script src="jquery.fill.js"></script>
<script>
        $.fill.options.color = "yellow";
        $.fill();
        $("div").first().fill({ color: "blue" })
        .end().last().fill({ color: "green" });
        $.title($.title() + "$$");
</script>
 
</body>
</html>

測試成功!

接著我們把JavaScript抽出來改寫為TypeScript,一如預料,馬上遇到JQuery、JQueryStatic未定義fill、title的錯誤,無法編譯。

為了讓TypeScript認識我們的Plugin,我們需在jquery.fill.ts加入interface JQuery及interface JQueryStatic宣告,TypeScript會將它們合併到JQuery定義檔的同名interface中,如此JQuery會多了.fill()以支援$("…").fill()語法、JQueryStatic會增加.fill()、.title()以支援$.fill()、$.title()。不過有個地方比較麻煩,由於要同時支援$.fill()、$.fill.options兩種存取方式,需多宣告一個interface JQFillStatic,其中包含一個方法(options?: JQFillOptions)以及一個屬性options,而JQueryStatic interface的fill型別為JQFillStatic,如此才能讓$.fill()與$.fill.options都有效。宣告程式如下:

/** jquery.fill options */
interface JQFillOptions {
/** fill color */
    color?: string;
}
interface JQuery {
/**
     * 將元素填滿背景色
     * 
     * @param options 顏色設定,未提供時依預設值
     */
    fill(options?: JQFillOptions): JQuery;
}
interface JQFillStatic {
/**
     * 將document.body填滿背景色
     * 
     * @param options 顏色設定,未提供時依預設值
     */
    (options?: JQFillOptions);
/** 顏色預設值 */
    options?: JQFillOptions;
}
interface JQueryStatic {
    fill?: JQFillStatic;
/** 
     * 取得或設定document.title
     * @param title 要設定的網頁標題,未提供時傳回現有標題
     */
    title(title?: string);
}
 
(function ($) {
///...省略...
})(jQuery);
 

如此,TypeScript就認得我們的Plugin囉~

在以上案例,JQuery Plugin用TypeScript開發,因此JQuery、JQueryStatic宣告直接寫入同一TypeScript檔即可。如果是第三方JavaScript,做法則比照scripts/typings/jquery/jquery.d.ts,要為該JavaScript檔寫一個同檔名的.d.ts。更進一步,還可將你寫好的定義檔貢獻到DefinitelyTyped,分享給開發社群,這才是新時代的好男兒! (遠目)

劍及履及,我的第一個TypeScript定義檔已經被Merge到DefintelyTyped,DefinitelyTyped會自動將其包成NuGet Package,所以jQuery BlockUI現在有TypeScript定義檔囉~

關於撰寫定義檔,TypeScript CodePlex上有篇教學: Writing Definition (.d.ts) Files,如果你也有心參與DefinitelyTyped的定義檔補完計劃,可以參考貢獻指南

NG筆記11-依條件決定是否加入DOM

$
0
0

這回輪到使用NG演練KO範例8 - if及with的應用

Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 8 - ng-if應用</title>
<style>
    span { margin: 5px; }
</style>
</head>
<bodyng-controller="defaultCtrl">
<ul>
<ling-repeat="player in model.players">
<span>{{player.name}}</span>
<!-- 
沒有bestRecord時隱藏<span>區塊,但是其中的內容還是會被解析,
不過NG對繫結屬性設定有較高容錯力,不會拋出JavaScript錯誤
            -->
<spanng-hide="!player.bestRecord">
                (最佳成績: 
<spanng-bind="player.bestRecord.score | number:0"></span>
<spanng-bind="player.bestRecord.date"></span>)
</span>
</li>
</ul>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function player(name) {
var self = this;
          self.name = name;
          self.bestRecord = null;
          self.setBestRecord = function (d, s) {
              self.bestRecord = {
                  date: d, score: s
              };
          };
      }      
function myViewModel() {
var self = this;
        self.players = [];
//加入兩筆測試資料
//第一筆資料有最佳成績(含日期及分數)
var p1 = new player("Darkthread");
        p1.setBestRecord("2012-06-01", 65535);
        self.players.push(p1);
//第二筆資料無最佳成績
var p2 = new player("Jeffrey");
        self.players.push(p2);        
      }
      $scope.model = new myViewModel();
    });
</script>
</body>
</html>

使用ng-hide/ng-show時,即使沒有資料不需顯示,DOM元素存在但為隱藏狀態。但與KO有點不同,當player.bestRecord == undefined或null時,data-bind="text: bestRecord.date"會引發錯誤,在NG僅為無資料不致出錯。

若希望條件不成立時不要在DOM出現,可使用ng-if: Live Demo

<ul>
<ling-repeat="player in model.players">
<span>{{player.name}}</span>
<spanng-if="player.bestRecord">
                (最佳成績: 
<spanng-bind="player.bestRecord.score | number:0"></span>
<spanng-bind="player.bestRecord.date"></span>)
</span>
</li>
</ul>

至於KO的with,NG並沒有對應的Directive。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記12-ngSwitch與Scope繼承

$
0
0

前篇介紹過ng-if,動態決定是否產生DOM區塊,NG還有另一項工具 – ngSwitch!

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>ng-switch範例</title>
<style>
    .sw > div { padding: 6px; margin: 6px; }
</style>
</head>
<bodyng-controller="defaultCtrl">
<selectng-model="model.view">
<option>A</option><option>B</option><option>C</option>
</select>
  propA = {{propA}} model.propB = {{model.propB}}
<divng-switch="model.view"class="sw">
<divng-switch-when="A">
      View A / propA={{propA}} propB={{model.propB}}
</div>
<divng-switch-when="B">
      View B / <br/>
      propA <inputng-model="propA"/><br/>
      propB <inputng-model="model.propB"/>
</div>
<divng-switch-default>
      View C
</div>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
        self.view = "A";
        self.propB = "Boo";
      }
      $scope.propA = "Foo";
      $scope.model = new myViewModel();
    });
</script>
</body>
</html>

Live Demo

ngSwitch用法如上,在容器加上ng-switch="屬性",用ng-switch-when="特定值"控制子元素是否顯示。切換下拉選單,下方會分別顯示View A、View B或View C。在此範例中,我額外加了propA及propB用來突顯NG的Scope繼承特性。propA直屬$scope,而propB屬於$scope.model,在View B用兩個<input> ng-model繫結,允許使用者修改propA及propB。

當使用某些Directive(包含: ng-repeat, ng-include, ng-switch, ng-view, ng-controller,或是其他scope: true或transclude: true的自訂Directive),NG會為其建立一個新Scope並繼承父容器Scope的屬性。以前述程式為例,每當ng-switch-when="A"及ng-switch-when="B"成立要顯示<div>時,NG會建立新Scope,並繼承父容器Scope自動加入兩個屬性: propA(字串)以及propB(物件)。propA是字串(屬Primitive原始型別)、propB則是JavaScript物件,如同C# Value Type與Reference Type間的差異,父Scope與新Scope的propA是兩個字串,propB卻指向同一個物件。切到View B,動手修改propA及propB,可發現最上方的propA不受影響,propB跟著連動,證明父Scope.propA與View B Scope.propA是兩個獨立物件,而父Scope.model與View B Scope.model則指向相同物件。

另外還有一個重要觀念,為View B建立的新Scope在切換到View A或View C後便消失,下次再切回View B時,會再產生另一個新Scope。在View B將propA改成FooX,切到View C再切回View B,propA又變回Foo,驗證剛才的修改已隨風而逝。

NG的Scope繼承行為,常把初學者耍得團團轉直呼見鬼,為此官方特別寫了一份文件(Understanding Scopes · angular-angular.js Wiki)做了詳細解釋,值得一讀。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記13-事件繫結

$
0
0

使用NG復刻KO範例9 - 事件繫結

Live Demo

<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 9 - 事件繫結</title>
<style>
      .block
      {
          width: 50px; height: 50px;
          border: 2px dotted gray; cursor: pointer;
          float: left; text-align: center;
          margin-right: 10px; background-color: white;
          line-height: 50px;
      }
</style>
</head>
<bodyng-controller="defaultCtrl"ng-style="{ backgroundColor: model.bgColor }">
<div>
<divclass="block"ng-repeat="color in model.colors"
ng-style="{ color: color }"
ng-mouseover="model.miceOver(color)"
ng-mouseout="model.miceOut()"
>
        {{ color }}
</div>
</div>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script>
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
function myViewModel() {
var self = this;
var origBgColor = "#444";
        self.colors = ["Red", "Green", "Blue"];
        self.bgColor = origBgColor;
        self.miceOver = function (data) {
            self.bgColor = data;
        };
        self.miceOut = function () {
            self.bgColor = origBgColor;
        };        
      }
      $scope.model = new myViewModel();
    });
</script>
</body>
</html>

只有兩點補充:
1) ng-style="{ prop1:value1, prop2: value2 }"可指定Inline Style
2) ng-mouseover, ng-mouseout事件

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs
Viewing all 2456 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>