Extjs: Component 的使用,找尋 Parent & Child,以及區別 Id、itemid 的不同


了解 component 的運作將有助於在操作 extjs 時,能夠更靈活操作在一個頁面中各個元件的運作,有了 component 查詢父類以及子類的方法可以更方便存取各階層的元件,特別在 single page 的頁面設計下更顯重要,並且在前端程式架構與分工上我們將可以很輕易的將 view 與事件獨立開來,避免重覆設計元件,造成維護上的困擾。

下面將說明查詢父類以及子類的方式

component hierarchy (階層)

取得特定 component

取得 parent component

因為 extjs 所有的元件皆繼承於 component,且 Ext.Container 擁有 component,故通常透過下列方式取得的 parent Component 都屬於 Ext.Container,雖然如此,parent 物件還是擁有當初創建 Component 的屬性或方法。

  • Component.ownerCt

    ownerCt 為 Component 的 properties,因此可直接取得,詳細可查找 extjs api

1
parent = child.ownerCt;
1
2
3
4
var form=this.findParentBy(function(p) {
  //定義你要索引的 parent 的條件式,若回傳為 true 將會回傳該 container parent
  return p.getForm;
});

主要透過 findParentBy 實作,找出最接近且符合傳入 xtype

1
2
3
4
5
6
7
8
9
function (xtype) {
  return Ext.isFunction(xtype) ?
    this.findParentBy(function(p){
        return p.constructor === xtype;
    }) :
    this.findParentBy(function(p){
        return p.constructor.xtype === xtype;
    });
}

取得 child component

  • getComponent
1
2
3
4
var childPanel = Ext.getCmp('parentPanel').getComponent('childPanel09');
if (childPanel) {
  alert('yes. child exists');
}
  • find
1
2
3
4
var childPanel = Ext.getCmp('parentPanel').find('id', 'childPanel09')[0]; // [0] because find returns array
if (childPanel) {
  alert('yes. child exists');
}
  • findBy

    使用方式同 findParentBy

  • findByType

    使用方式同 findParentByType

id 與 itemid 的不同

從官方的 Ext.Component-cfg-id 以及 Ext.Component-cfg-itemId 的說明可以看到文件內容記載,為了避免翻譯上的落差,使用中英文對照:

id

節錄比較重要的部分,全文請參考官方 API

id 是作為 component 的 唯一識別,如果沒有設置 id 將會自動產生 id。

The unique id of this component (defaults to an auto-assigned id).

必須注意的是 id 會作為 html 中 element 的 id,一旦該物件已經被 rendered。

Note that this id will also be used as the element id for the containing HTML element that is rendered to the page for this component.

如果你有需要改變 component 的 css 就可以透過 id 來進行操作,如果也需要改變 component 底下的 子元件也必須使用 id 來操作。

This allows you to write id-based CSS rules to style the specific instance of this component uniquely, and also to select sub-elements using this component’s id as the parent.

官方文件說明的很清楚,我就不再補述了。接著我們在來看 itemid 的說明。

itemid

itemid 可以作為參照 component 的替代方案

An itemId can be used as an alternative way to get a reference to a component when no object reference is available.

其中:

  • id 可用於 Ext.getCmp
  • itemid 用於 Ext.Container.getComponent

區別兩者不同,並且善用能夠取得設定該 config 相關的元件

Instead of using an id with Ext.getCmp, use itemId with Ext.Container.getComponent which will retrieve itemId’s or id’s.

一旦 itemid 設置於 container 底下的任何物件 (MixedCollection),則 itemId 他的範圍 (Scope) 將只限於 container 並且是區域性的。

Since itemId’s are an index to the container’s internal MixedCollection, the itemId is scoped locally to the container

最後為了避免淺在衝突,若是使用 Ext.ComponentMgr 必須要有唯一的 id 可進行識別

avoiding potential conflicts with Ext.ComponentMgr which requires a unique id.

從上面的敘述中可以看出一些使用上需注意的地方

  1. 若你要取得相關的 Component 若是屬於 container 請設置 id
  2. 若是只屬於某個 container 底下的元件,請設置 itemId;某些情況下當然你也可以連同 id 一起設置,比如需要改變 css style,除此之外 itmeid 還是優先的選擇
  3. getCmp 以及 getComponent 是不一樣的,使用上必須注意,必須搭配 id 以及 itemid 使用

如此一來 id 與 itemid 就會有從屬關係,非必要不需定義 id,也不會造成在定義 id 時需要編碼避免重覆,而因為 itemid 是屬於某個 id 底下的,所以即使 itemid 重覆,也可以利用唯一的 id 利用 Ext.getCmp 取得參照後,在接著使用 getComponent 取得所屬 itemId 的參照。

實際的使用參考官方範例:

var c = new Ext.Panel({ //
    height: 300,
    renderTo: document.body,
    layout: 'auto',
    items: [
        {
            itemId: 'p1',
            title: 'Panel 1',
            height: 150
        },
       {
        itemId: 'p2',
            title: 'Panel 2',
            height: 150
        }
    ]
})
p1 = c.getComponent('p1'); // not the same as Ext.getCmp()
p2 = p1.ownerCt.getComponent('p2'); // reference via a sibling

除了上述 id 以及 itemid 可用於索引元件取得參照之外,我們還可以使用以下方法

ref (extjs 3.x)

如果你所使用的 extjs 版本為 3.x,利用 ref 可以更加方便得取得 container 底下的 Component,連 getComponent 都可以免了。

參考官方文件 Ext.Component-cfg-ref

ref 是使用路徑的語法,該路徑相關於 Component 的 ownerCt,ownerCt 所參照的是該物件的所屬的 Container 也就是他的上層父元件

A path specification, relative to the Component’s ownerCt specifying into which ancestor Container to place a named reference to this Component.

從上面的敘述有個概念後,來看實例會更清楚 ref 的作用,如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
    // 自定義數一個元件,繼承於 Ext.form.FormPanel
    MyComponent = Ext.extend(Ext.form.FormPanel, {
        frame: true,

        initComponent: function() {

            // 這個元件擁有兩個欄位,以及兩個 button
            var config = {
                items: [{
                            xtype: 'textfield',
                            fieldLabel: 'Name'
                        }, {
                            xtype: 'textfield',
                            fieldLabel: 'Address'
                        }],
                /* 
                * 對於 bbar 裡的 button 使用 ref
                * 在這層結構上為 panel > toolbar > button
                * 為了改變為 panel > button
                * 所以以 button 的 ownerCt 為 root 也就是 toolbar 的情況下
                * 在往上一層就如同上述的結構透過 ../ 將層級提昇
                */
                bbar: ['->', {
                            text: 'Cancel',
                            minWidth: 100,
                            ref: '../cancelButton'
                        }, {
                            text: 'Save',
                            minWidth: 100,
                            ref: '../saveButton'
                        }]
            };

            //將 initComponent 裡設置好的初始 config 與 initialConfig 合併作為初始的元件
            Ext.apply(this, Ext.apply(this.initialConfig, config));

            //執行 superclass.initComponent 並且指定自定義好的元件作為 this 
            MyComponent.superclass.initComponent.apply(this, arguments);

        }
    });

    //對新元件註冊 xtype 透過 Ext.reg
    Ext.reg('my_component_xtype', MyComponent);


    // Create a display a window with the panel in it...

    var w = new Ext.Window({
                modal: true,
                items: {
                    // 一旦 my_component_xtype 也就是 MyComponent
                    // 被實體化後就會有兩個欄位與兩個 button
                    xtype: 'my_component_xtype',
                    title: 'Panel 1',
                    // 在這邊的階層為 window > panel
                    ref: 'theFormPanel'
                }
            });
    w.show();


    // See how we can use the references...
    // 因為在 MyComponent 改變了 ref 往上一層跳過 toolbar
    // 所以我們就可以如以下操作該物件

    w.theFormPanel.saveButton.on('click', function() {
                console.log('Save was clicked');
            }, this);

詳細註解與說明都在上面程式的註解裡,這邊就不多做說明,可以看到透過 ref 的使用,我們可以更加方變得操作 extjs 裡的元件。除了 id 以及 itemid 之外也多了一個可以更方便敘述物件關係的方式。

在這篇文章寫好之前,上面的差異我從來都不知道,也這樣用了好幾年… 說來慚愧,使用一個新的語言或框架基礎還是很重要的。

另外 ref 在 extjs 3.x 還存在,但在 extjs 4.x 已拿掉此屬性,不過別擔心,官方有給我們更好的物件選擇方式,up,down,以及 ComponentQuery

up,down,以及 ComponentQuery

使用 ComponentQuery,妳可以利用類似 jquery 的語法來進行查詢 extjs 元件,舉例來看,假設我們有個元件宣告如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
Ext.define('MyApp.view.MyViewport', {
    extend: 'Ext.container.Viewport',

    id: 'mainvp',
    layout: {
        align: 'stretch',
        type: 'vbox'
    },

    initComponent: function() {
        var me = this;

        Ext.applyIf(me, {
            items: [
                {
                    xtype: 'gridpanel',
                    flex: 1,
                    title: 'My Grid Panel',
                    store: 'MyArrayStore',
                    dockedItems: [
                        {
                            xtype: 'toolbar',
                            dock: 'top',
                            items: [
                                {
                                    xtype: 'button',
                                    text: 'remove'
                                },
                                {
                                    xtype: 'button',
                                    text: 'add'
                                }
                            ]
                        }
                    ],

                    columns: [
                        {
                            xtype: 'gridcolumn',
                            dataIndex: 'id',
                            text: 'Id',
                            editor: {
                                xtype: 'textfield'
                            }
                        },
                        ...
                    ],
                    listeners: {
                        select: {
                            fn: me.onGridpanelSelect,
                            scope: me
                        }
                    }
                },
                {
                    xtype: 'form',
                    flex: 1,
                    itemId: 'form1',
                    bodyPadding: 10,
                    title: 'form1',
                    items: [
                        {
                            xtype: 'textfield',
                            anchor: '100%',
                            fieldLabel: 'id',
                            name: 'id'
                        },
                        {
                            xtype: 'textfield',
                            anchor: '100%',
                            fieldLabel: 'name',
                            name: 'name'
                        }
                    ]
                },
                {
                    xtype: 'form',
                    flex: 1,
                    itemId: 'form2',
                    bodyPadding: 10,
                    title: 'form2',
                    items: [
                        {
                            xtype: 'textfield',
                            anchor: '100%',
                            fieldLabel: 'title',
                            name: 'title'
                        }
                    ]
                }
            ]
        });

        me.callParent(arguments);
    }

});

呈現畫面如下:

image

這樣的例子中,可以看到,在底下有兩個 panel,如果我們要操作第二個 form,在上面的程式碼中我們將其 itemid 設為 form2,如此一來我們可以利用下列程式來取得參照

Ext.ComponentQuery.query('panel[itemId=form1]');

當然除了 itemid,只要是屬於該物件的屬性皆可以拿來進行查詢,比如說我們自定一個屬性為 cls:form2-cls,那我們就可以依樣畫葫蘆:

Ext.ComponentQuery.query('panel[cls=form2-cls]');

就是如此方便!除了 Ext.ComponentQuery.query 另外還有提供兩個好用的方法 up()down(),同樣以上面為例子,來進行 remove 的實作,一旦點選 remove 按鈕時要將在 grid 中選中的 record 移除,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
    xtype: 'button',
    handler: function(button, event) {
          var grid = button.up().up();
        var sm = grid.getSelectionModel();

        button.up().up().store.remove(sm.getSelection());

        if (button.up().up().store.getCount() > 0) {
            sm.select(0);
        }
    },
    text: 'remove'
}

文章中有提到 extjs 會記住使用到的各個 Component 以及之間的關係,因此一旦能夠存取到事件發動的物件妳就可以從該物件找到互動的物件,參考範例的介面,元件結構上為 grid > toolbar > button,因此一旦我點選了 button 我要找到 grid 只要往上兩層 button.up().up() 即可,down() 的部份同理。

除了單純的使用之外,還可以搭配 ComponentQuery 語法使用,假設妳目前已可以存取 grid,要取得 button 的參照,妳可以這樣做:

1
grid.down("button[itemId=updateBtn]")

即使層級上不只一層,透過妳下的條件,extjs 會找出符合條件最接近的物件,如此一來妳就可以輕鬆遊走各個元件之間了!