Salesforce LWC學習(四) 父子component互動 / component宣告週期管理 / 事件處理
我們在上篇介紹了 @track / @api的區別。在父子 component中,針對api型別的變數,如果宣告以後就只允許在parent修改,son component修改便會導致報錯。
sonItem.html
1 <template> 2 <lightning-input value={itemName} onchange={changeItemName} label="item name"></lightning-input> 3 </template>
sonItem.js
1 import { LightningElement, api } from 'lwc'; 2 3 export default class SonItem extends LightningElement { 4 @api itemName = 'test'; 5 6 changeItemName() { 7 this.itemName = 'change item name'; 8 } 9 }
parentForSonItem.html
1 <template> 2 <c-son-item item-name="test parent for son item"></c-son-item> 3 </template>
執行結果:預設顯示一個輸入框,當修改了裡面內容,開啟console以後會顯示報錯,原因為sonItem這個 component嵌在了parentForSonItem這個父component中聲明瞭api註解,所以針對itemName只能允許parentForSonItem去更新,然後傳遞到子component。如果我們想子修改後影響api宣告的public變數,就只能通過事件處理方式傳播給父component,讓父去做修改的事情。這個後續會有類似的demo。
如果我們單獨把sonItem放在佈局中,改變item name便可以正常的觸發事件並且沒有報錯資訊。
一. 父子component互動
在專案中我們針對一個大的component/app設計時,可能有多個component組合在一起,比如我們在salesforce lightning零基礎學習(十一) Aura框架下APP構造實現 這篇中,針對一個最終的功能頁面可能有N個component進行組合從而實現,這種設計的好處是很多component都是可重用的。針對LWC中針對這種component組合有幾個概念。下面是例舉的一個官方的demo。根據層次結構,在LWC中有幾個概念:
Owner:Owner代表當前Own這個template的component,我們可以理解成當前的最高級別的component。當前的component中,todoItem嵌入在了todoWrapper中,todoWrapper嵌在了todoApp中,所以針對當前的component,todoApp是這幾個component的owner。針對owner的 component有以下的功能:
- 針對他包含的component可以設定public變數,這裡我們可以設定todoItem的item-name這個public變數(itemName在todoItem中宣告為api型別);
- 可以呼叫包含的component中的方法;
- 當包含的component設定了事件情況下,owner的component可以監聽到。
Container:Container代表當前這個component包含了其他的component,但是當前的component還在其他的component中。下面的demo中我們可以看到todoWrapper包含了todoItem,但是todoWrapper還被todoApp包含著,所以針對這個demo中,todoWrapper是container。針對container的component有以下的功能:
- 可以讀到包含的component中的public的變數,但是是隻讀的,沒法編輯;
- 可以呼叫包含的component中的方法;
- 可以監聽到bubble型別的對應的所包含的component事件。(事件可以分為bubble/capture)
1 <!-- todoApp.html --> 2 <template> 3 <c-todo-wrapper> 4 <c-todo-item item-name="Milk"></c-todo-item> 5 <c-todo-item item-name="Bread"></c-todo-item> 6 </c-todowrapper> 7 <template>
Parent and Child:當一個component只包含一個子component時,形成了父子模型,todoApp為父,todoItem為子。按照上面的模型,todoApp為owner,具有owner的功能。
1 <!-- todoApp.html --> 2 <template> 3 <c-todo-item item-name="Milk"></c-todo-item> 4 <c-todo-item item-name="Bread"></c-todo-item> 5 </template>
我們在上一篇和這一篇demo中已經介紹過如何去針對Owner設定child component的public變數,此篇中講parent/owner component如何呼叫child component的方法。這裡先介紹一下html中的selector的概念。
Selector:下面的這張截圖大家很常見也很好懂,我們在宣告一個css時經常會寫成類似這種,左側代表一組選擇器,右側代表宣告的css規則塊。css selector可以理解成CSS rule中左側的Group of selectors.
Selector可以分成不同的型別:
- Simple selectors: 基於element type來匹配一個或者多個元素,比如使用class或者id;
- Attribute selectors: 基於attribute或者attribute value來匹配一個或者多個元素;
- Pseudo-classes:匹配一個或者多個處於特定狀態的元素,例如滑鼠指標懸停在其上的元素、當前被禁用或選中的複選框或是DOM樹中其父級的第一個子級元素等等;
- Pseudo-elements: 匹配一個元素的某個位置的一個或者多個內容。比如每個段落的第一個字等。
Simple selectors我們在專案中經常用到的就是標籤選擇器,class選擇器,id選擇器。
1 <style type="text/css"> 2 p { 3 background:green; 4 } 5 6 .spanClass { 7 background:red; 8 } 9 10 #spanId { 11 background:yellow; 12 } 13 </style> 14 15 <p>標籤選擇器</p> 16 <span class="spanClass">class選擇器</span> 17 <span id="spanId">id選擇器</span>
Attribute Selector我們在專案中常用的就是基於屬性精確或者模糊設定CSS樣式。
1 <style type="text/css"> 2 [data-vegetable] { 3 color: green; 4 } 5 6 [data-vegetable="liquid"] { 7 background-color: goldenrod; 8 } 9 10 [data-vegetable~="spicy"] { 11 color: red; 12 } 13 </style> 14 15 <ul> 16 <li data-quantity="700g" data-vegetable="not spicy like chili">Red pepper</li> 17 <li data-quantity="2kg" data-meat>Chicken</li> 18 <li data-quantity="optional 10ml" data-vegetable="liquid">Olive oil</li> 19 </ul>
Pseudo-classes我們經常會在專案中處理一些偽類的處理,比如針對超連結的懸停,active等的處理。針對此種類型,我們通常在一個selector後面使用' : '關鍵字。
1 <style type="text/css"> 2 a { 3 color: blue; 4 font-weight: bold; 5 } 6 7 a:visited { 8 color: blue; 9 } 10 </style> 11 12 <a href="https://trailhead.salesforce.com" target="_blank">trailhead</a>
Pseudo-elements和Pseudo-classes用法很像,區別是關鍵字是' :: '.用來獲取一個元素的一部分內容。demo中展示的是如果href後面中以https起始,則新增⤴。
1 <style type="text/css"> 2 [href^=https]::after { 3 content: '⤴'; 4 } 5 </style> 6 7 <ul> 8 <li><a href="https://test.com">HTTPS</a> demo will show ⤴</li> 9 <li><a href="http://test.com">HTTP</a> demo will not show ⤴</li> 10 </ul>
四種常用的css selector介紹完了,下面就引出querySelector以及querySelectorAll的概念。
querySelector方法是一個標準的DOM API,作用為針對匹配的selector返回第一個元素。salesforce建議我們儘量不要使用ID作為selector,因為當template中使用ID的時候,瀏覽器渲染以後ID將會變成一個global 的唯一的key。如果我們使用ID作為selector,將可能無法正常匹配上。
querySelector詳情可以檢視:https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector
我們針對一個數組的迭代,比如我們針對複選框使用class作為selector可能需要返回多個元素,這個時候我們就要使用querySelectorAll。此方法用來返回的是所有的匹配的selector的元素。querySelector我們可以檢視:https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll。
有人會提出疑問,繞了這麼半天,我可以使用document或者window的global方法啊,比如document.getElementById或者document.querySelector?原因為因為locker原因,LWC不允許使用window或者document這個global 變數,所以替代方案為使用this.template.querySelector()替代document.querySelector()。
使用querySelector/querySelectorAll有幾點注意事項:
- 針對返回的多個數據,元素的順序無法保證;
- 使用querySelector時,如果元素沒有在DOM中渲染的無法搜尋出來,我們在後面會有component生命週期管理的內容,當子component沒有渲染載入或者當前在建構函式沒有渲染出來的時候,使用querySelector是無法查詢出來的;
- querySelector不要使用ID作為selector。
下面就通過一個例子來了解針對parent/owner如何去訪問son component的方法。
currentTime.html: 用來將傳進來Date變數format成指定格式的日期,預設獲取的是當前的時間;
1 <template> 2 當前時間: 3 <lightning-formatted-date-time 4 value={currentTime} 5 year="numeric" 6 month="numeric" 7 day="numeric" 8 hour="2-digit" 9 minute="2-digit" 10 second="2-digit" 11 time-zone-name="short" 12 > 13 </lightning-formatted-date-time> 14 </template>
currentTime.js:宣告一個變數用於前臺展示,宣告的方法必須要使用@api標籤才可以供owner/parent component進行方法呼叫。
1 import { LightningElement,track,api } from 'lwc'; 2 3 export default class CurrentTime extends LightningElement { 4 @track currentTime = new Date(); 5 6 @api refreshTime() { 7 this.currentTime = new Date(); 8 } 9 }
showCurrentTime.html:引入current-time,並且放一個按鈕
1 <template> 2 <c-current-time></c-current-time> 3 <lightning-button 4 label="Refresh Time" 5 onclick={handleRefresh} 6 ></lightning-button> 7 </template>
showCurrentTime.js:使用querySelector獲取到current-time這個元素,然後呼叫其方法。這裡的template變數用於在javascript中訪問元件中渲染的元素。
1 import { LightningElement } from 'lwc'; 2 3 export default class ShowCurrentTime extends LightningElement { 4 handleRefresh() { 5 this.template.querySelector('c-current-time').refreshTime(); 6 } 7 }
上述的demo中實現的就是最基本的使用querySelector實現獲取子component並且呼叫子component方法的例子。其他更多細節歡迎自行檢視文件。
二. LWC針對component的生命週期管理
LWC針對component載入以及移除有一套生命週期管理機制,針對不同生命週期的節點我們可以做不同的事情,也有不同的限制。
針對component載入渲染的生命週期管理圖如下所示:
1. 針對有父子關係巢狀的component,先執行parent component的constructor()方法,針對constructor方法,有幾點需要注意:
- 第一個語句必須是super()並且不帶引數,宣告以後便可以使用了this關鍵字;
- 在constructor方法裡面不要使用return語句去return什麼返回值,除非是針對某些邏輯下直接返回不執行下面可以使用return 或者return this,其他不允許;
- 和上面的querySelector相同,不允許使用document以及window;
- 不要檢查元素的attribute以及他們的子元素,因為這個階段他們還不存在;
- 不要檢查元素中的使用@api宣告的public 變數,因為他們在component建立以後才可以引用;
2. 檢視public變數是否有等待被更新的,如果有,更新public 變數的值;
3. Parent Component插入進DOM中,當插入完會觸發parent component的connectedCallback()方法,這個時候因為parent componentyi經插入完畢,所以此方法中可以呼叫parent component中對應的element等資訊,我們可以使用this.template去訪問相關的element。通過方法描述可以看出來,此方法可能不止呼叫一次,當DOM中有新插入的component便會觸發此方法。比如我們動態搜尋資料,list資料可能會變化或者reorder,會呼叫此方法多次;
4. 當connectedCallbacl()方法執行完以後,parent component渲染完成;
5. 此時子component會自動觸發建構函式constructor()方法;
6.檢視子component中的變數是否有被等待更新的,如果有,更新public 變數的值;
7. 子component插入進DOM中,插入完成後會呼叫connectedCallback()方法;
8.子component渲染完成;
9. 當父子component都渲染完成以後,父component呼叫renderedCallback()方法。
針對component移除的生命週期管理圖如下所示:
當parent component從DOM移除時,會觸發parent component的disconnectedCallback方法;
當son component從DOM移除時,會觸發son component的disconnectedCallback方法。
當我們瞭解了LWC針對component的生命週期管理,我們便可以更好的針對每個做不同的處理,當然,我們很多時候會將生命週期管理和事件管理一起使用。接下來的內容為LWC的事件管理。
三. LWC 事件管理
對Aura事件不瞭解或者對web標準的事件管理不瞭解的可以先看一下salesforce lightning零基礎學習(五) 事件階段(component events phase),LWC和他們有很多相似之處。最開始的demo中我們演示了針對@api的public變數,子component不能修改其變數值,如果子真的有必要修改如何做呢?那就建立一個事件並且去通知其父元件。父元件對這個事件進行監聽,然後父元件去更改這個值並且重新渲染會子元件從而實現了子元件修改變數值的訴求。
在LWC中,Event基於DOM Event,感興趣的小夥伴可以讀一下https://dom.spec.whatwg.org/#events,裡面包括了很多的object以及相對應的API方法。當建立Event的時候,官方推薦使用customEvent,因為其擁有更好的相容性以及更多的功能,同時他封裝了detail變數,我們在事件處理中可以使用此變數去傳遞任意型別的資料。Event事件管理可以進行以下的幾步走。
1. 建立事件
我們使用CustomEvent()去新建一個自定義的事件,此建構函式由兩個引數,第一個引數傳遞的是事件名稱,第二個引數是CustomEventInit,是一個可選的設定項,此引數可以設定好幾個欄位。比如detail欄位用來在事件中傳遞處理中可以作為引數作為傳遞,bubbles來決定當前的事件處理是bubble還是capture,cancelable來決定當前事件觸發以後是否可以取消,composed來確定是否會觸發shadow DOM 根節點以外的事件監聽器。當然我們在使用中可能常用的就是設定detail用來傳遞引數以及bubble來設定傳播方式。
2. 排程事件
當我們自定義完事件以後,我們需要排程此事件才可以正常的進行事件監聽。使用this.dispatchEvent(eventName)即可排程,排程以後,會根據custom event設定的傳播方式對父子component進行排程。排程順序不懂的可以檢視上面的事件階段的部落格。
3. 事件監聽處理
當事件建立並且在子component排程完成後,父component便需要進行事件監聽處理。LWC提供了兩種方式進行事件監聽。一種是在父component引入子component時直接在其template上新增監聽器的標籤,另外一種是通過js方式設定監聽器,很像我們的瀏覽器標準事件監聽處理方式。
component標籤方式:比如我們建立了一個自定義的事件名稱為notification在child的component,我們在parent component引入並且想要設定此事件的監聽處理方法為handleNotification方法,我們只需要使用on + 自定義事件名稱即可實現事件監聽處理,這也就是上一篇中介紹為什麼不能以on作為變數開頭的原因。
1 <template> 2 <c-child onnotification={handleNotification}></c-child> 3 </template>
js方式:我們在父componet的初始化的方法中,使用addEventListener方法去實現事件監聽,第一個引數是自定義的事件名稱,第二個是要事件處理的方法。
1 import { LightningElement } from 'lwc'; 2 export default class Parent extends LightningElement { 3 constructor() { 4 super(); 5 this.template.addEventListener('notification', this.handleNotification.bind(this)); 6 } 7 }
通過上面的三步走,我們便完成了針對事件處理的基本瞭解。但是我們疑問還是特別多,比如針對事件處理的方法,我能做什麼?針對Event是否有什麼封裝好的方法可以讓我更好的去運用? 大家在aura學習事件處理的時候應該很有了解,salesforce lightning零基礎學習(九) Aura Js 淺談二: Event篇 aura提供了我們針對事件處理的一系列的方法。LWC的custom event大部分使用的是DOM原生的,所以DOM 原生Event也封裝好了很多的變數以及方法,想要詳細瞭解的小夥伴可以檢視:https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent,下面只例舉部分常用變數。
detail:detail變數可以獲取到事件宣告的時候傳遞的引數資訊,傳遞的引數型別可以為任何的型別。下面的demo中傳遞了一個簡單的object包含了isSelected,在事件處理中我們便可以使用event.detail獲取到當前傳遞的引數。
1 const selectEvent = new CustomEvent('select', { 2 detail: {isSelected:true}3 }); 4 this.dispatchEvent(selectEvent); 5 6 const isSelected = selectEvent.detail.isSelected;
bubbles:返回的是一個布林型別的變數,判斷當前的事件宣告的是bubble還是capture。如果是bubble則返回true,capture則返回false。
currentTarget:我們事件排程以後,會根據bubble/capture順序去執行相關的handler,currentTarget指定了當前正在處理該事件的元素。
target:獲取我們當前正在執行的事件最開始排程的元素。他和currentTarget是有區別的,currentTarget永遠指定的是當前正在處理該事件的元素。target指定的是最開始排程的元素。
紙上學來終覺淺,絕知此事要躬行。下面以一個官方提供的簡單的demo去更好的瞭解事件處理。
ContactController.cls:此方法封裝了一個簡單的查詢語句然後返回資料列表
1 public with sharing class ContactController { 2 3 @AuraEnabled(cacheable=true) 4 public static List<Contact> getContactList() { 5 return [SELECT Id, Name, Title, Phone, Email FROM Contact LIMIT 10]; 6 } 7 }
contactListItem.html:作為item,顯示contact name,點選以後呼叫handleClick方法。
1 <template> 2 <a href="#" onclick={handleClick}> 3 {contact.Name} 4 </a> 5 </template>
contactListItem.js:在handleClick方法中聲明瞭自定義事件並且對事件進行了排程。
1 import { LightningElement, api } from 'lwc'; 2 3 export default class ContactListItem extends LightningElement { 4 @api contact; 5 6 handleClick(event) { 7 // 1. Prevent default behavior of anchor tag click which is to navigate to the href url 8 event.preventDefault(); 9 // 2. Read about event best practices at http://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.events_best_practices 10 const selectEvent = new CustomEvent('select', { 11 detail: this.contact.Id 12 }); 13 // 3. Fire the custom event 14 this.dispatchEvent(selectEvent); 15 } 16 }
eventWithData.html:迭代顯示資料,並且針對引入的子component設定了監聽處理的方法。當子component點選觸發事件,執行handleSelect方法獲取選中的contact然後渲染出來隱藏的詳情區域。
1 <template> 2 <lightning-card title="EventWithData" icon-name="standard:logging"> 3 <template if:true={contacts.data}> 4 <lightning-layout class="slds-m-around_medium"> 5 <lightning-layout-item> 6 <template for:each={contacts.data} for:item="contact"> 7 <c-contact-list-item 8 key={contact.Id} 9 contact={contact} 10 onselect={handleSelect} 11 ></c-contact-list-item> 12 </template> 13 </lightning-layout-item> 14 <lightning-layout-item class="slds-m-left_medium"> 15 <template if:true={selectedContact}> 16 17 <p>{selectedContact.Name}</p> 18 <p>{selectedContact.Title}</p> 19 <p> 20 <lightning-formatted-phone 21 value={selectedContact.Phone} 22 ></lightning-formatted-phone> 23 </p> 24 <p> 25 <lightning-formatted-email 26 value={selectedContact.Email} 27 ></lightning-formatted-email> 28 </p> 29 </template> 30 </lightning-layout-item> 31 </lightning-layout> 32 </template> 33 34 </lightning-card> 35 </template>
eventWithData.js:此方法
1 import { LightningElement, wire, track } from 'lwc'; 2 import getContactList from '@salesforce/apex/ContactController.getContactList'; 3 4 export default class EventWithData extends LightningElement { 5 @track selectedContact; 6 7 @wire(getContactList) contacts; 8 9 handleSelect(event) { 10 const contactId = event.detail; 11 this.selectedContact = this.contacts.data.find( 12 contact => contact.Id === contactId 13 ); 14 } 15 }
顯示效果:
預設顯示列表資料,當點選一個contact name以後,會建立並且排程事件,eventWithData監聽到事件以後,執行監聽處理方法,設定selectedContact。此變數使用track標籤宣告,所以改變以後會重新渲染eventWithData component實現細節的展示。
官方在 github.com/trailheadapps/lwc-recipes提供了很多的學習的demo,感興趣的可以檢視。更多生命週期的demo可以檢視git demo中的pubsubContactList。
總結:篇中只是介紹了父子通過querySelector獲取相關的element實現互動以及component生命週期管理和事件的簡單處理。篇中有錯誤的地方歡迎指出,有問題歡迎留言。更多學習內容還要自行參看官方文