自定義元素提供了一種將組件注入視圖的方便方法。
本節目錄
- 介紹
- 例子
- 傳遞參數
- 父組件和子組件之間的通信
- 傳遞監控屬性的表達式
介紹
自定義元素是組件綁定的語法替代(實際上,自定義元素使用后臺的組件綁定)。
例如,一個繁瑣寫法的示范:
<div data-bind='component: { name: "flight-deals", params: { from: "lhr", to: "sfo" } }'></div>
其實可以更簡單:
<flight-deals params='from: "lhr", to: "sfo"'></flight-deals>
示例
這個例子聲明了一個組件,然后將它的兩個實例注入到一個視圖中。 請參閱下面的源代碼。
First instance, without parameters
Second instance, passing parameters
UI源碼:
<h4>First instance, without parameters</h4> <message-editor></message-editor> <h4>Second instance, passing parameters</h4> <message-editor params='initialText: "Hello, world!"'></message-editor>
視圖模型代碼:
ko.components.register('message-editor', { viewModel: function(params) { this.text = ko.observable(params.initialText || ''); }, template: 'Message: <input data-bind="value: text" /> ' + '(length: <span data-bind="text: text().length"></span>)' }); ko.applyBindings();
注意:在更現實的情況下,通常從外部文件加載組件視圖模型和模板,而不是將它們硬編碼到注冊中。
傳遞參數
正如您在上面的示例中看到的,您可以使用params屬性為組件視圖模型提供參數。 params屬性的內容被解釋為類似于JavaScript對象字面值(就像數據綁定屬性一樣),因此您可以傳遞任何類型的任意值。 例:
<unrealistic-component params='stringValue: "hello", numericValue: 123, boolValue: true, objectValue: { a: 1, b: 2 }, dateValue: new Date(), someModelProperty: myModelValue, observableSubproperty: someObservable().subprop'> </unrealistic-component>
父組件和子組件之間的通信
如果在params屬性中引用模型屬性,那么當然是指組件外部的viewmodel(“parent”或“host”viewmodel)上的屬性,因為組件本身尚未實例化。 在上面的示例中,myModelValue將是父視圖模型上的一個屬性,并且將被子組件viewmodel的構造函數接收為params.someModelProperty。
這是如何將屬性從父視圖模型傳遞到子組件。 如果屬性本身是可觀察的,則父視圖模型將能夠觀察并對子組件插入的任何新值做出反應。
傳遞可觀察的表達式
在以下示例中,
<some-component params='simpleExpression: 1 + 1, simpleObservable: myObservable, observableExpression: myObservable() + 1'> </some-component>
...組件viewmodel params參數將包含三個值:
simpleExpression
-
這將是數字值2.它不會是可觀察值或計算值,因為沒有涉及可觀察值。
一般來說,如果參數的求值不涉及對可觀察量的求值(在這種情況下,該值不涉及可觀察量),那么該值將按字面意義傳遞。如果值是一個對象,那么子組件可以改變它,但是由于它不可觀察,所以父組件不會知道子組件已經這樣做。
-
simpleObservable
-
這將是在父viewmodel上聲明為myObservable的ko.observable實例。它不是一個包裝器 - 它是父母引用的實際相同的實例。因此,如果子viewmodel寫入此observable,父viewmodel將接收到該更改。
一般來說,如果一個參數的求值不涉及一個可觀察值的計算(在這種情況下,觀察值被簡單地傳遞而不對其進行求值),那么這個值被字面傳遞。
-
observableExpression
-
表達式本身,當被評估時,讀取一個observable。該observable的值可能隨時間而變化,因此表達式結果可能會隨時間而變化。
為了確保子組件能夠對表達式值的更改做出反應,Knockout會自動將此參數升級為計算屬性。因此,子組件將能夠讀取params.observableExpression()以獲取當前值,或使用params.observableExpression.subscribe(...)等。
一般來說,對于自定義元素,如果參數的求值涉及評估一個可觀察量,則Knockout自動構造一個ko.computed值以給出該表達式的結果,并將其提供給該組件。
-
總之,一般規則是:
- 如果參數的求值不涉及可觀察/計算的計算,則按字面意義傳遞。
- 如果參數的求值涉及到計算一個或多個可觀察量/計算,它將作為計算屬性傳遞,以便您可以對參數值的更改做出反應。
將標記傳遞到組件中
有時,您可能需要創建接收標記并將其用作其輸出的一部分的組件。例如,您可能想要構建一個“容器”UI元素,例如網格,列表,對話框或標簽集,可以接收和綁定內部的任意標記。
考慮可以如下調用的特殊列表組件:
<my-special-list params="items: someArrayOfPeople"> <!-- Look, I'm putting markup inside a custom element --> The person <em data-bind="text: name"></em> is <em data-bind="text: age"></em> years old. </my-special-list>
默認情況下,<my-special-list>中的DOM節點將被剝離(不綁定到任何viewmodel)并由組件的輸出替換。 但是,這些DOM節點不會丟失:它們被記住,并以兩種方式提供給組件:
- 作為數組$ componentTemplateNodes,可用于組件模板中的任何綁定表達式(即作為綁定上下文屬性)。 通常這是使用提供的標記的最方便的方法。 請參見下面的示例。
- 作為一個數組,componentInfo.templateNodes,傳遞給它的createViewModel函數
組件可以選擇使用提供的DOM節點作為其輸出的一部分,但是它希望,例如通過對組件模板中的任何元素使用template:{nodes:$ componentTemplateNodes}。
例如,my-special-list組件的模板可以引用$ componentTemplateNodes,以使其輸出包括提供的標記。 下面是完整的工作示例:
Here is a special list
-
Here is another one of my special items
UI源碼:
<!-- This could be in a separate file --> <template id="my-special-list-template"> <h3>Here is a special list</h3> <ul data-bind="foreach: { data: myItems, as: 'myItem' }"> <li> <h4>Here is another one of my special items</h4> <!-- ko template: { nodes: $componentTemplateNodes, data: myItem } --><!-- /ko --> </li> </ul> </template> <my-special-list params="items: someArrayOfPeople"> <!-- Look, I'm putting markup inside a custom element --> The person <em data-bind="text: name"></em> is <em data-bind="text: age"></em> years old. </my-special-list>
視圖模型源碼:
ko.components.register('my-special-list', { template: { element: 'my-special-list-template' }, viewModel: function(params) { this.myItems = params.items; } }); ko.applyBindings({ someArrayOfPeople: ko.observableArray([ { name: 'Lewis', age: 56 }, { name: 'Hathaway', age: 34 } ]) });
這個“特殊列表”示例在每個列表項上面插入一個標題。 但是相同的技術可以用于創建復雜的網格,對話框,選項卡集等,因為這樣的UI元素所需要的是常見的UI標記(例如,定義網格或對話框的標題和邊框) 提供標記。
當使用沒有自定義元素的組件時,即當直接使用組件綁定時傳遞標記,這種技術也是可能的。
控制自定義元素標記名稱
默認情況下,Knockout假定您的自定義元素標記名稱完全對應于使用ko.components.register注冊的組件的名稱。 這種約定超配置策略是大多數應用程序的理想選擇。
如果你想要有不同的自定義元素標簽名稱,你可以覆蓋getComponentNameForNode來控制這個。 例如,
ko.components.getComponentNameForNode = function(node) { var tagNameLower = node.tagName && node.tagName.toLowerCase(); if (ko.components.isRegistered(tagNameLower)) { // If the element's name exactly matches a preregistered // component, use that component return tagNameLower; } else if (tagNameLower === "special-element") { // For the element <special-element>, use the component // "MySpecialComponent" (whether or not it was preregistered) return "MySpecialComponent"; } else { // Treat anything else as not representing a component return null; } }
如果要控制哪些已注冊組件的子集可以用作自定義元素,則可以使用此技術。
注冊自定義元素
如果你使用默認的組件加載器,因此使用ko.components.register注冊你的組件,那么沒有什么額外的你需要做。 以這種方式注冊的組件可以立即用作自定義元素。
如果你實現了一個自定義組件加載器,并且沒有使用ko.components.register,那么你需要告訴Knockout你想要用作自定義元素的任何元素名稱。 為此,只需調用ko.components.register - 您不需要指定任何配置,因為您的自定義組件加載器將不會使用配置。 例如,
ko.components.register('my-custom-element', { /* No config needed */ });
或者,您可以覆蓋getComponentNameForNode以動態控制哪些元素映射到哪些組件名稱,而與預注冊無關。
備注1:將自定義元素與常規綁定相結合
如果需要,自定義元素可以具有常規的數據綁定屬性(除了任何params屬性)。 例如,
<products-list params='category: chosenCategory' data-bind='visible: shouldShowProducts'> </products-list>
但是,使用將修改元素內容的綁定(例如,文本或模板綁定)是沒有意義的,因為它們會覆蓋您的組件注入的模板。
Knockout將阻止使用任何使用controlsDescendantBindings的綁定,因為當嘗試將其viewmodel綁定到注入的模板時,這也會與組件發生沖突。 因此,如果要使用if或foreach等控制流綁定,則必須將其包裝在自定義元素周圍,而不是直接在自定義元素上使用,例如:
<!-- ko if: someCondition --> <products-list></products-list> <!-- /ko -->
或者
<ul data-bind='foreach: allProducts'> <product-details params='product: $data'></product-details> </ul>
備注2:自定義元素不能自行關閉
您必須寫入<my-custom-element> </ my-custom-element>,而不是<my-custom-element />。否則,您的自定義元素不會關閉,后續元素將被解析為子元素。
這是HTML規范的限制,不在Knockout可以控制的范圍之內。 HTML解析器遵循HTML規范,忽略任何自閉合斜杠(除了少量的特殊“外來元素”,它們被硬編碼到解析器中)。 HTML與XML不同。
注意:自定義元素和Internet Explorer 6到8
Knockout努力讓開發人員處理跨瀏覽器兼容性問題的痛苦,特別是那些與舊版瀏覽器相關的問題!即使自定義元素提供了一種非常現代的web開發風格,他們仍然可以在所有常見的瀏覽器上工作:
- HTML5時代的瀏覽器,包括Internet Explorer 9和更高版本,自動允許自定義元素沒有困難。
- Internet Explorer 6到8也支持自定義元素,但前提是它們在HTML解析器遇到任何這些元素之前注冊。
IE 6-8的HTML解析器將丟棄任何無法識別的元素。為了確保不會丟棄您的自定義元素,您必須執行以下操作之一:
- 確保在HTML解析器看到任何<your-component>元素之前調用ko.components.register('your-component')
- 或者,至少在HTML解析器看到任何<your-component>元素之前調用document.createElement('your-component')。你可以忽略createElement調用的結果 - 所有重要的是你已經調用它。
例如,如果你像這樣構造你的頁面,那么一切都會OK:
<!DOCTYPE html> <html> <body> <script src='some-script-that-registers-components.js'></script> <my-custom-element></my-custom-element> </body> </html>
如果你使用AMD,那么你可能更喜歡這樣的結構:
<!DOCTYPE html> <html> <body> <script> // Since the components aren't registered until the AMD module // loads, which is asynchronous, the following prevents IE6-8's // parser from discarding the custom element document.createElement('my-custom-element'); </script> <script src='require.js' data-main='app/startup'></script> <my-custom-element></my-custom-element> </body> </html>
或者如果你真的不喜歡document.createElement調用的hackiness,那么你可以使用一個組件綁定為你的頂層組件,而不是一個自定義元素。 只要所有其他組件在您的ko.applyBindings調用之前注冊,他們可以作為自定義元素在IE6-8生效:
<!DOCTYPE html> <html> <body> <!-- The startup module registers all other KO components before calling ko.applyBindings(), so they are OK as custom elements on IE6-8 --> <script src='require.js' data-main='app/startup'></script> <div data-bind='component: "my-custom-element"'></div> </body> </html>
高級應用:訪問$ raw參數
考慮以下不尋常的情況,其中unObservable,observable 1和observable 2都是可觀測量:
<some-component params='myExpr: useObservable1() ? observable1 : observable2'> </some-component>
由于評估myExpr涉及讀取observable(useObservable1),KO將向組件提供參數作為計算屬性。
但是,計算屬性的值本身是可觀察的。這似乎導致一個尷尬的情況,其中讀取其當前值將涉及雙解開(即,params.myExpr()(),其中第一個括號給出表達式的值,第二個給出的值結果可見實例)。
這種雙重解開將是丑陋,不方便和意想不到的,所以Knockout自動設置生成的計算屬性(params.myExpr)來為你解開它的值。也就是說,組件可以讀取params.myExpr()以獲取已選擇的可觀察值(observable1或observable2)的值,而不需要雙重解開。
在不太可能發生的情況下,您不想自動解包,因為您想直接訪問observable1 / observable2實例,您可以從params。$raw讀取值。例如,
function MyComponentViewModel(params) { var currentObservableInstance = params.$raw.myExpr(); // Now currentObservableInstance is either observable1 or observable2 // and you would read its value with "currentObservableInstance()" }
這應該是一個非常不尋常的情況,所以通常你不需要使用$raw。
文章列表