The unsafe resource URL and cross origin on angular

在angular很常遇到compile template時出錯的情況,因為在angular中,使用比較嚴謹的判斷方式,只要看到不符合規範,會直接拋出exception

##Cross origin

以下是一個video的範例,設定影片的source:

1
2
3
<video width="320" height="240" controls>
<source src="{{source}}" type="video/mp4">
</video>

設定source的影片來源:

1
$scope.source = "http://other.sparrowhome.twbbs.org/movie.mp4"

這時就會發現angular拋出exception,出現cross domain的問題。

但是若是直接寫在html上呢?

1
2
3
<video width="320" height="240" controls>
<source src="http://other.sparrowhome.twbbs.org/movie.mp4" type="video/mp4">
</video>

此時你可以發現能正常的播放,但是在angular就是會拋錯,這時你可以透過$sceDelegateProvider,來設定黑名單白名單,決定哪個domain可以讀取這隻影片。

假設你要讓所有domain都能使用,在白名單可以這樣寫:

1
2
3
4
5
angular
.module('MyApp')
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist(['**']);
})

若是你只要目前網域底下的子網域都可以讀取的話,可以這樣寫:

1
$sceDelegateProvider.resourceUrlWhitelist(['http://*.sparrowhome.twbbs.org'])

如果要透過黑名單可以直接使用resourceUrlBlacklist

##Unsafe resource URL

如果是讀取img,不管是cross domain都能正常讀取,不過還是會有不讀取的狀況。

例如你透過一個<input type="file">,去取得顯示image,像是<img ng-src="{{img}}" />

1
$scope.img = URL.createObjectURL(imgFile);

此時就會拋出unsafe:blobexception,依據URL.createObjectURL所取得url,可以很容易發現,最前方協定是不同的。例如URL.createObjectURL回傳一個blob:http%3A%2F%2Fsparrowhome.twbbs.org%2Fimage.png,此時不在預設的允許名單中,這時就要透過$compileProvider.imgSrcSanitizationWhitelist去改寫。

像是讓所有協定都通過:

1
2
3
4
angular.module('MyApp').config(['$sceDelegateProvider', function( $compileProvider ) {

$compileProvider.imgSrcSanitizationWhitelist(/^.*/);
}

也可自訂要通過特定的協定,像是/^(https?|ftp|file|blob|data):/,同樣的href也有aHrefSanitizationWhitelist可以去設定。在angular中使用要嚴謹的判斷方式,當然也可透過jquery或原生javascript繞過這問題,不過這樣就有點失去用angular的意義了,畢竟用template的方式,比起寫code跟為簡潔。

The ngAnimate on angular(1.2.13)

angular的ngAnimate主要是依照不同的Directive,對照不同的class做切換,以下有一張官方提供的對照表:

Directive Supported Animations
ngRepeat enter,leave and move
ngView enter and leave
ngInclude enter and leave
ngSwitch enter and leave
ngIf enter and leave
ngClass add and remove
ngShow & ngHide add and remove (the ng-hide class value)

比較值得注意的是,angular(1.2)以上的版本,是依據directive的目前的status,替換不同的class,它會在每個不同的status加上ng-....的class,如ng-enterng-leave等的。

ngRepeat為例,定義一個html結構:

1
2
3
4
5
<ul>
<li ng-repeat="role in roles" class="demo">
{{role}}
</li>
</ul>

加入一個值到roles

1
$scope.roles.push( "programer" );

則此時會先插入一個ng-enterclass,做初始配置用,接著在插入ng-enter-active的class,改變後的狀態,只有設定transition,這時就會產生動畫效果。

依照element的class名稱,css的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.demo.ng-enter {

-webkit-transition: all 1s linear;
transition: all 1s linear;

/**
* set styles
*/

}
.demo.ng-enter-active {
/**
* set styles
*/

}

若是不支援css3的瀏覽器,則必須自己實作動畫效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
app.animation('.demo', function() {

return {

enter:function( element, done ){

doEffect( done );

return function( cancelled ) {

if ( cancelled ) {

//stopEffect();

} else {

//completeTheAnimation();
}

};
}

};

});

如果要使用到jquery來做特效,需在angular載入之前,先行載入jquery,因為angular會判斷要使用jqueryjqlite,然後再做一些處理。若要使用jquery改變顏色,則需要額外載入jquery-color。另外如果同時用javascript和css實作特效,則css特效會被javascript所覆蓋。

Example

Install

1
bower install angular angular-animate jquery jquery-color

Create a angular app and load a ngAnimate module( app.js ):

1
var app = angular.module('angularNgApp', ["ngAnimate"]);

html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="main.css">
</head>
<body ng-app="angularNgApp">

<div ng-controller="MainCtrl">
<ul>
<li ng-repeat="thing in awesomeThings" class="demo">
{{thing}}
</li>
</ul>
</div>

<!--[if lt IE 9]>
<script src="bower_components/es5-shim/es5-shim.js"></script>
<script src="bower_components/json3/lib/json3.min.js"></script>
<![endif]-->



<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/jquery-color/jquery.color.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-animate/angular-animate.js"></script>
<!-- endbower -->

<script src="app.js"></script>
<script src="controller.js"></script>
<!--[if lt IE 9]>
<script src="animation.js"></script>
<![endif]-->


</body>
</html>

main.css:

1
2
3
4
5
6
7
8
9
10
.demo.ng-enter {

-webkit-transition: all 1s linear;
transition: all 1s linear;
background: #000;
}

.demo.ng-enter-active {
background: #fc3;
}

controller.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.controller('MainCtrl', function ( $scope, $timeout ) {

$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];

$timeout( function(){

$scope.awesomeThings.push("123");

}, 2200);

});

animation.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.animation('.demo', function() {

return {
enter : function( element, done ) {

element.animate({
'background-color': 'red'
}, done);

return function( cancelled ) {

if(cancelled) {
//stopTheAnimation();
}
else {
//completeTheAnimation();
}
}
}
};
});

Capture a image block on angular

最近需要使用到一個圖片切割的工具,在angular的package中找了一下,沒有找到比較合適,因為只需要支援html5的瀏覽器,就實作了一個間單的工具(ngImageEditor)。

以下是demo實際執行的畫面:

explain

使用與安裝:

Demo

demo

Install

1
bower install ngImageEditor

Support

  • IE9+
  • chrome
  • firefox

設定editor的attributes

1
<div img-src="imgSrc" ng-image-editor="imageEditor" selected="selected"></div>

載入要切割的圖片

1
$scope.imgSrc='/images/head.jpeg';

設定切割的區塊位置及大小

1
$scope.selected = {width:150,height:150,top:0,left:0};

將區塊大小的image取出

1
$scope.imageEditor.toDataURL();

實作方式主要是透過canvas作轉換,至於cross domain,必須是同個domain,否則要從server上設定,允許哪個domain可以存取作編輯。

在server上設定你要跨網域的domain:

1
Access-Control-Allow-Origin: http://your.domain.com

在javascript中呼叫img.src之前,加入以下設定:

1
img.crossOrigin = "Anonymous";

可參考這篇

jquery detach on angular directive

在angular中會有基本的jquery-lite,也就是angular.element,當在include angular之前,就先include jquery,此時angular的jquery-lite,就會被自動替換成jquery。

替換angular.element的source code如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function bindJQuery() {
// bind to jQuery if present;
jQuery = window.jQuery;
// reset to jQuery or default to us.
if (jQuery) {
jqLite = jQuery;
extend(jQuery.fn, {
scope: JQLitePrototype.scope,
isolateScope: JQLitePrototype.isolateScope,
controller: JQLitePrototype.controller,
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
// Method signature:
// jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
jqLitePatchJQueryRemove('remove', true, true, false);
jqLitePatchJQueryRemove('empty', false, false, false);
jqLitePatchJQueryRemove('html', false, false, true);
} else {
jqLite = JQLite;
}
angular.element = jqLite;
}

另外一個需要注意的地方就是jqLitePatchJQueryRemove,會將jquery原生的function,多經過一層處理,處理完在呼叫jquery的function

jqLitePatchJQueryRemove的source如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) {     
var originalJqFn = jQuery.fn[name];
originalJqFn = originalJqFn.$original || originalJqFn;
removePatch.$original = originalJqFn;
jQuery.fn[name] = removePatch;

function removePatch(param) {
// jshint -W040
var list = filterElems && param ? [this.filter(param)] : [this],
fireEvent = dispatchThis,
set, setIndex, setLength,
element, childIndex, childLength, children;

if (!getterIfNoArguments || param != null) {
while(list.length) {
set = list.shift();
for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) {
element = jqLite(set[setIndex]);
if (fireEvent) {
element.triggerHandler('$destroy');
} else {
fireEvent = !fireEvent;
}
for(childIndex = 0, childLength = (children = element.children()).length;
childIndex < childLength;
childIndex++) {
list.push(jQuery(children[childIndex]));
}
}
}
}
return originalJqFn.apply(this, arguments);
}
}

其中element.triggerHandler('$destroy')會告知angular底下的transcludedScope,這個element已經被摧毀了,所以會導致,$element.detach的時候,transcludedScope無法被更新。element.triggerHandler('$destroy')的監聽event可以參考createBoundTranscludeFn這個function,會有下這段code去監聽:

1
clone.on('$destroy', bind(transcludedScope, transcludedScope.$destroy));

jqLitePatchJQueryRemove只替換removeemptyhtml,而沒有替換detach,使用detach卻會影響scope,真正原因要找jquery的source code。

因為detach間接呼叫了remove:

1
2
3
detach: function( selector ) {
return this.remove( selector, true );
}

Example(1.2.7):

由此可知,只要scope尚未ready時,呼叫detach就不會中斷scope的更新。若要讓directive的scope正常運作,可用以下方式:

  1. 不使用transclude
  2. 不在scope ready中,使用detach
  3. 使用原生api
  4. 最後一種方式,jquery在angular之後include

如果不是這麼需要使用到jquery,建議就不要include了,當然jquery也提供很多方便的功能,依照project需求自己評估吧!

angularjs timepicker on bootstrap3

angularjs的ui-bootstrap,套用至bootstrap3,目前只能支援部份的元件。所以就自己實作了一個timepicker的元件,如下:

Dependency

  • boostrap3
  • angularjs

Support

  • IE8+
  • chrome
  • firefox

Install

用bower install

1
bower install ngTimepickerForBs3

Usage

include所需要的scripts和styles:

1
2
<link rel="stylesheet" href="bower_components/ngTimepickerForBs3/angularjs-timepicker.css"/>
<script type="text/javascript" src="bower_components/ngTimepickerForBs3/angularjs-timepicker.js"></script>

設定timepicker的model和初始值;

1
<div ng-timepicker-for-bs3 ng-model="test" ng-init="test={hours:23,mins:59}"></div>

載入ngTimepickerForBs3的module:

1
2
3
angular.module( "app", [
"ngTimepickerForBs3"
]);

Angularjs directive uses transclude attribute

使用directive時,會將符合restrict的element都做初始化,directive指向的element包含child-element,這時候就要設定transclude:true,將child-element,添加到template裡,可以使用ng-transclude或者透過directive定義controller去處理。

透過ng-transclude處裡child element

定義好tag的結構,button-group為預設tag:

1
2
3
4
5
<div>
<button-group>
<button>Enter</button>
</button-group>
</div>

透過ng-transclude,將原先在button-group裡的child-element添加至ng-transclude裡面:

1
2
3
4
5
6
7
8
9
10
11
app.directive( "buttonGroup", function(){

return {

restrict: 'EA',
template: '<div class="btn-group" ng-transclude ></div>',
replace: true,
transclude: true
};

});

最後就會取得到底下的結果:

1
2
3
4
5
<div>
<div class="btn-group" ng-transclude="">
<button class="ng-scope">Enter</button>
</div>
</div>

透過directive的controller添加child element

使用$transclude取得clone的element,在append至需要添加的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
controller: ['$scope', '$element', '$transclude', function ($scope, $element, $transclude) {

$transclude(function(clone) {

var primaryBlock = $element.find('div.primary-block');
var transcludedButtons = clone.filter(':button');

angular.forEach(transcludedButtons, function(e) {

if (angular.element(e).hasClass('primary')) {
primaryBlock.append(e);
}

});
});
}]

使用ngTransclude的Demo

完整的範例,請參考jsfiddle上的code。

Angularjs directive uses attributes of scope

在angularjs中的directive的scope,分別有以下3種設定方式:

  • = or =attr : 指定需要連結的model名稱,會與parent scope的model互相綁定。
  • @ or @attr : 依照屬性的value傳入,來設定directive的scope值。
  • & or &attr : 將預設的function,委託給其他event呼叫。

只要directive有設定scope對應的值,或者設定scope:true,則directive將會自行建立一個新的scope。

使用 & 和 @ 屬性來設定scope

定義一個alert屬性:

1
<div alert on-enter="$window.alert('Hello world')" message="Say hello world!"></div>

其中message屬性為alert呈現的訊息,on-enter則會在按鈕被點擊即時會觸發。

實作一個alert directive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.directive( "alert", function(){

return {

restrict: "A",
template: "<div class='alert-container'><p ng-bind="message"></p><button ng-click='onEnter();'>Enter</botton></div>",
scope:{
"onEnter":"&",
"message":"@"
},
replace: true

};

});

alert的scope將會取得onEntermessage的屬性,分別去對照template的ng-click="onEnter()"ng-bind="message"

Demo

完整的範例,請參考jsfiddle上的code。

在angularjs限制同時間的request的數量

假設有多筆request要發送時,但又不希望一次全發送出去,希望將request的數量,在同個時段只出現5個request,這時候就需要一個manager之類的object去處理,在這裡使用ngHttpPool為例。

安裝ngHttpPool

使用bower安裝,或者至github下載:

1
bower install ngHttpPool

使用ngHttpPool

載入一個ngHttpPool module:

1
<script type="text/javascript" src="/bower_components/ngHttpPool/public/src/ngHttpPool.js"></script>

定義module相依性:

1
2
3
4
5
angular.module( "app", [ 
"ngHttpPool"
]).controller('ctrl',function( httpPool ){
//doSomething
});

建立一個pool和定義同時存在的最大request數量:

1
2

var pool = httpPool.create( 2 );

傳入config,發送多個request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

for( var index = 0; index<10 ; index++ ){

/**
* @param {Object} config
* @param {function} success is optional
* @param {function} error is optional
*/
pool.map({method:"get",url:"/",params:{id:index}}, function(){

console.log( "success" );

});

}

如果要等request都從server返回,可使用以下的方式:

建立一個defer,會將之後發送request都當作監聽對象:

1
2

pool.listen()

將config push到pool,執行request的發送:

1
2
3
4
5

for( var index = 0; index<10 ; index++ ){

pool.map({method:"get",url:"/",params:{id:index}});
}

結束監聽,當所有結果回傳時,將會執行promise.then:

1
2
3
4
5
6

var promise = pool.Promise();

promise.then(function(){
console.log( "all of the callbacks." );
});

Demo

可參考ngHttpPool github。

angularjs-placeholder submit a form on IE

使用angularjs-placeholder可以在老舊IE上,支援placeholder這個屬性,但是底層還是去實作foucsblur這兩個event,並且用替換value的方式,達到模擬placeholder的效果。在一般使用上沒什麼太大問題,但是假設今天有一個formsubmit的時候,會把placeholder的預設值也一起送出,這時就必須在submit之前,就將預設值清除,或者在取值得時候,暫時將欄位值清除,之後在還原。

使用placeholder.ensure取值:

ensure接受兩個參數:

1
2
3
4
5
6
7

/**
* @param {jQlite} $elem 可以是form或者為多個elements
* @param {function} callback 實際取值的地方
*/


placeholder.ensure( $elem, callback );

callback裡面取得值後,可使用record.back來回復placeholder的預設值。

1
2
3
4
5
6
7
8
9
10
11
12
app.controller( "formController", [ "placeholder", "$element", function( placeholder, $form ){

placeholder.ensure( $form , function( record ){

var $textarea = $form.find("textarea");
alert( $textarea );

//復原
record.back();
});

});

若要直接submit可直接呼叫ensure,不須使用callback。

1
placeholder.ensure( $form )

如果使用有支援placeholder的瀏覽器,還是會有ensure可以使用,但是將不會改變任何屬性,純粹只是個包裝過後的function。

Fixed a placeholder feature on IE (angularjs)

Placeholder要支援老舊的IE7~IE9,使用jQuery也都有一堆plugin可以使用了。如果要在angularjs上使用,但是又不想載入jQuery,這時候可以使用angularjs-placeholder module。

使用angularjs-placeholder

載入angular module:

1
<script type="text/javascript" src="../src/angularjs-placeholder.js"></script>

定義需要使用到的module,和初始化ng-app:

1
2
3
4
<script type="text/javascript">
var app = angular.module( "demoApp", ["html5.placeholder"] );
angular.bootstrap( document, [app.name] );
</script>

實作angularjs-placeholder遇到的小問題

在實作angularjs-placeholder module時,在IE7發現一個小問題,無法取得placeholder的值,但是在有的jQuery情況,都會幫忙處理掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* input default: <input placeholder="Please,enter your name!" type="text" />
*/


//use native

var attr = input.getAttribute("placeholder");

// attr == null
console.log( attr );

//use jqlite
var $elem = angular.element( input );

// attr == null
console.log( $elem.attr( "placeholder" ) );

所以如果要在IE7取得placeholder,這時候native有提供另一個api可以使用。

1
2
3
4
5
6
var node = input.getAttributeNode( "placeholder" );

var attr = node?node.nodeValue:node;

//Please,enter your name!
console.log( attr );