我们在上篇介绍了 @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 component已经插入完毕,所以此方法中可以调用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:此方法封装了一个变量,使用wire Service调用后台的controller去获取数据展示,当选中子以后调用handleSelect方法执行事件监听处理。wire service后期会有单独篇去讲。
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生命周期管理和事件的简单处理。篇中有错误的地方欢迎指出,有问题欢迎留言。更多学习内容还要自行参看官方文档。