[AngularJS] ng-admin介紹

介紹

當我們要寫一個後台系統的時候,也就兩種方法可以選,一種是全部自己來,一種是拿著別人寫好的樣板來套,今天要來介紹的就是基於angularjs的一套後台系統ng-admin

選擇一個系統當然要看看有沒有什麼好處囉,所以我們來看一下有些什麼功能

  • restful api
  • CRUD,透過設定的方式來完成
  • editor

其實非常基本的功能,但還是要介紹下去…已經在這個系統上花了不少時間orz

初始

首先要先知道這套系統,基本上不太需要寫太多複雜的程式,只要透過類似設定的方法,就可以做到CRUD,下面來看看該怎麼做

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
angular.module('app', ['ng-admin'])
.config(['NgAdminConfigurationProvider', function(nga){
// application:網站名稱
// baseApiUrl:api end-point
var admin = nga.application('My First Admin')
.baseApiUrl('http://jsonplaceholder.typicode.com/');

var user = nga.entity('users');
//定義list畫面要顯示的欄位
user.listView().fields([
nga.field('name'),
nga.field('username'),
nga.field('email')
])
//刪除和編輯的功能
.listActions(['edit', 'delete']);
//定義create畫面要輸入的欄位
user.creationView().fields([
nga.field('name'),
nga.field('username'),
nga.field('email')
]);
//定義edit畫面要輸入的欄位
user.editionView().fields([
nga.field('username'),
nga.field('email')
]);

admin.addEntity(user);
nga.configure(admin);
}]);

前面有說到這是一個引用restful的admin,所以我們要先定義基本的end-point,接著就是定義要顯示的欄位以及真正的資料來源entity,可以透過上面這個範例看到,非常的簡單,我們其實只是用定義欄位的方法就完成了CRUD

end-point

基本上打出去的api會是這個模式,當然這套也提供客製request或是response的功能,在這邊我就不特別提到,需要的可以去翻一下參考文件

name type end-point
listView GET /users
creationView POST /users
showView GET /users/:id
editionView PUT /users/:id
deletionView DELETE /users/:id

欄位資料關連

在admin中很常見到的是要關連另一個資料,例如post中要選擇user,這時候我們可以透過field的關聯欄位來完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var user = nga.entity('users');
var post = nga.entity('posts');

post.creationView().fields([
nga.field('name'),
//使用reference
nga.field('userId', 'reference')
//定義關連的資料
.targetEntity(user)
//定義要顯示的欄位,預設對應欄位是userId=id
.targetField(nga.field('username'))
//顯示的label
.label('user'),
nga.field('email'),
]);

欄位型態

#客製

基本的功能在參考資料中可以找到很多,其實官方也寫了很多的說明,告訴你要怎麼使用,以下是官方所支援的所有類型,詳細的使用方法就到各個詳細連結中看囉

但你一定會有那種需求是上面都做不到的(我就有一個T_T,那我們要怎麼來自定義這些功能呢

自定欄位型態

首先的首先,就是這邊一定要用es6的寫法了,所以當你看到這邊,趕快把你的專案加上babel的編譯吧!
在這邊我要做的是當另一個欄位值改變時,要將下拉選單的清單更新,這邊我會使用reference作為基底去進行實作一個叫做relate的型態

這邊總共會寫三個檔案

  1. relateField.js,定義可以在nga.field()後面接續的欄位名稱

    就是這裡必須要用es6的class寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ReferenceField from 'admin-config/lib/Field/ReferenceField';
class RelateField extends ReferenceField {
constructor(name) {
super(name);
//必須要跟config中定義的名稱一樣
this._type = "relate";
this._referenceField = null;
}

referenceField(field) {
if (!arguments.length) return this._referenceField;
this._referenceField = field;

return this;
}
}

export default RelateField;

  1. relateFieldView.js,定義在各種情況下要怎麼呈現,我只在write的情況下改成自己的directive
1
2
3
4
5
6
export default {
getReadWidget: () => '<ma-reference-column field="::field" value="::value" datastore="::datastore"></ma-reference-column>',
getLinkWidget: () => '<ma-reference-link-column entry="::entry" field="::field" value="::value" datastore="::datastore"></ma-reference-link-column>',
getFilterWidget: () => '<ma-reference-field field="::field" value="value" datastore="::datastore"></ma-reference-field>',
getWriteWidget: () => '<relate field="::field" value="value" datastore="::datastore" entry="::entry"></relate>',
};
  1. relate.directive.js,定義directive的行為
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
38
39
40
41
function relate (ReferenceRefresher, locale) {
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?',
},
restrict: 'E',
template: '<ma-choice-field field="field()" datastore="datastore()" refresh="refresh($search)"' +
' value="value"></ma-choice-field>',
link: function (scope) {
const field = scope.field();
const referenceFieldId = field.referenceField().name();
scope.name = field.name();
scope.v = field.validation();
//主要的功能所在,監聽參考欄位是否變更
scope.$watch('entry.values.' + referenceFieldId, function (val) {
if (!val) {
scope.value = undefined;

return;
}

const search = {
[referenceFieldId]: scope.entry.values[referenceFieldId],
};
//這邊要特別注意,search的物件會直接變成query string
//像是這樣 _filters: {"name":{"region_id":2}}
ReferenceRefresher.refresh(field, scope.value, search)
.then(function (formattedResults) {
scope.$broadcast('choices:update', { choices: formattedResults });
});
});
},
};
}

relate.$inject = ['ReferenceRefresher', 'locale'];

export default relate;

都寫好以上的程式以後,只要在一開始的config宣告就可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular.module('app', ['ng-admin'])
.config(['NgAdminConfigurationProvider', 'FieldViewConfigurationProvider',
function(nga, fvp){
nga.registerFieldType('relate', RelateField);
fvp.registerFieldView('relate', RelateFieldView);

post.creationView().fields([
nga.field('name'),
nga.field('userId', 'relate')
.targetEntity(user)
.targetField(nga.field('username'))
//name欄位值變更時,要引發這邊的選單變更
.referenceField(nga.field('name'))
.label('user'),
]);
});

但是這邊稍微實驗一下,可以發現在directive中datastore是無法在一開始拿到users的資料,跟reference的行為有一點點的不一樣,追了一下source code以後,發現這是被包起來定義好的,所以沒辦法有相同的行為,但還好的是我不需要一開始就有資料,而是要等到referenceField變更以後再取得

結論

今天的介紹就到這邊,這套admin是一個蠻不一樣的架構,不希望你寫太多程式,而是透過設定的模版來達到需求,但是當今天功能越來越複雜,這套的限制就會越來越大,再客製上會有很大的侷限,如果已經知道未來會有很多變化(不管是欄位或是api),那建議就是不要使用這套,而是採用其他模版透過寫程式的方式最沒有侷限

參考