在 Lightning Web 组件中处理事件
学习目标
完成本单元后,您将能够:
- 创建包含多个组件的应用程序。
- 描述复杂组件的文件结构。
- 处理事件。
跟踪事件过程
您已经构建了一个组件并将其推送到组织中。让我们开始为事件处理添加一些交互性。我们通过若干组件跟踪事件的过程,以便在应用程序中处理复杂事件。此应用程序是自行车商店采用的产品选择器。用户通过点击自行车名称和图像来查看更多详细信息。
此应用程序有四个协同工作的组件。
-
tile(磁贴):显示单个项目。
-
list(列表):排列各磁贴。
-
detail(详细信息):点击一个磁贴时,会显示该磁贴的详细信息(与您刚创建的 bikeCard 相似)。
-
selector(选择器):包含整套组件。容器组件并非必需的,但我们在此处使用了一个容器组件,以帮助处理事件。
目前,该应用程序使用数据文件加载静态数据进行测试。在下一单元中,您将学习如何从组织中提取动态数据。
组件构成
让我们在项目中添加一些可以部署到组织的文件。
- 在此处下载此应用程序的文件:Trailhead 的自行车选择器应用程序。
- 将文件解压到 bikeCard 项目的 force-app/main/default/lwc 文件夹中。
组件关系
在此应用程序中,多个组件协同工作;某些组件嵌套在其他组件中。就像 HTML 元素相互嵌套一样,Lightning web 组件(即自定义 HTML 元素)也可以嵌套在其他 Lightning web 组件中。
在我们的文件系统中,组件的文件夹并不能真正深入了解它们之间的关系。
让我们来看看这些组件是如何在 UI 层面上以图表的形式嵌套的。
通过查看文件,您可以看到选择器组件布局了页面,并呈现了列表 (c-list
) 和详细信息 (c-detail
) 组件。
<template> <div class="wrapper"> <header class="header">Select a Bike</header> <section class="content"> <div class="columns"> <main class="main" > <c-list onproductselected={handleProductSelected}></c-list> </main> <aside class="sidebar-second"> <c-detail product-id={selectedProductId}></c-detail> </aside> </div> </section> </div> </template>
用以下内容更新 detail.html:
<template> <template lwc:if={product}> <div class="container"> <div>{product.fields.Name.value}</div> <div class="price">{product.fields.MSRP__c.displayValue}</div> <div class="description">{product.fields.Description__c.value}</div> <img class="product-img" src={product.fields.Picture_URL__c.value} alt={product.fields.Name.value}/> <p> <lightning-badge label={product.fields.Material__c.value}></lightning-badge> <lightning-badge label={product.fields.Level__c.value}></lightning-badge> </p> <p> <lightning-badge label={product.fields.Category__c.value}></lightning-badge> </p> </div> </template> <template lwc:else> <div>Select a bike</div> </template> </template>
在 detail.html 中,您可以看到条件渲染(lwc:if={product}
和 lwc:else
)。如果未从列表中选择任何内容,则会显示一条要求用户进行选择的消息。如果选择了某个内容,则会显示自行车信息。
列表组件呈现了多个磁贴 (c-tile
) 组件,数据中的每款自行车都对应一个组件。这种嵌套是在每个父级组件的 HTML 中实现的。例如,列表组件具有以下 HTML,包括磁贴组件 c-tile
。
<template> <div class="container"> <template for:each={bikes} for:item="bike"> <c-tile key={bike.fields.Id.value} product={bike} ontileclick={handleTileClick}></c-tile> </template> </div> </template>
请注意,自行车项目的每次迭代都会生成一个新的磁贴组件。只要包含 c-tile
组件标签,每个磁贴组件就会成为它的子级组件。Div 类定义“container”(容器)用于设置样式,因此您可以控制磁贴的排列。如果查看 list.css,便能看到它封装了内容。
.container { display: flex; flex-direction: row; flex-wrap: wrap; }
父级/子级关系对于应用程序的设计和事件处理都很重要。
让我们更深入地探讨一下事件处理。
事件向上,属性向下
在复杂组件(包含若干父级和子级组件)中,组件可以向上和向下交流。
- 子级组件 c-todo-item 向父级组件 c-todo-app 派发一个事件。例如,当用户点击按钮时,子级组件可以将事件对象传递给父级组件,这样父级组件就可以处理该事件并更改当前页面。
- c-todo-app 父级组件在子级组件中传递属性或调用方法。例如,父级组件可以在子级组件中设置文本值,或调用某个方法。
下面让我们来看看这种交流是如何进行的。
向上传递信息
可以使用 events(事件)和 event listeners(事件监听器)向上传递信息。
子级组件派发事件,父级组件监听事件。派发事件包括创建子级组件可以传递给父级组件的事件对象。父级组件有一个处理程序来响应该事件。
例如(不要创建这些组件),像这样的子级组件包含 nextHandler()
方法,该方法使用 CustomEvent()
创建一个简单的事件对象,并在用户点击 Next(下一步)按钮时派发事件类型 'next'。
// todoItem.js import { LightningElement } from 'lwc'; ... nextHandler() { this.dispatchEvent(new CustomEvent('next')); } }
父级组件用内联事件处理程序监听带有前缀 'on'(onnext) 的事件。
<!-- todoApp.html --> <template> <c-todo-item onnext={nextHandler}></c-todo-item> </template>
并将事件对象传递给事件处理程序。
// todoApp.js import { LightningElement } from 'lwc'; export default class TodoApp extends LightningElement { ... nextHandler(){ this.page = this.page + 1; } }
向下传递信息
可以使用 public properties(公共属性)和 public methods(公共方法)向下传递信息。
您可以使用 @api
装饰器将组件属性变为公共属性。然后通过外部组件来设置公共属性。
例如(不要创建这些组件),c-todo-item 子级组件具有以下内容:
// todoItem.js import { LightningElement, api } from 'lwc'; export default class TodoItem extends LightningElement { @api itemName; }
使用以下内容设置父级组件的值:
<!-- todoApp.html --> <template> <c-todo-item item-name="Milk"></c-todo-item> </template>
请注意,itemName
变量是使用串式命名法属性 item-name
来设置的。JavaScript 中的属性名称采用驼峰式大小写命名法,而 HTML 属性名称则为符合 HTML 标准的串式(用破折号分隔)。标记中的 item-name
属性映射到 itemName
JavaScript 属性。
公共属性是传递原始值、简单对象和数组的绝佳解决方案。
此外,当获取或设置属性时,您可以使用 getter 和 setter 来执行一些逻辑。记住要用 @api
装饰器对它们进行注释,使它们对其他组件公开。
类似地,您可以创建可从父级组件调用的公共方法。通过使用 @api
装饰器进行定义以在子级组件中创建一个公共方法,然后从父级组件中调用它。
假设我们有一个像这样的子级组件(不要创建这些组件)。
// videoPlayer.js import { LightningElement, api } from 'lwc'; export default class VideoPlayer extends LightningElement { @api play() { // Play music! } }
当 c-video-player 组件被包含在父级组件中时,我们就可以像这样从父级组件中调用该方法:
// methodCaller.js import { LightningElement } from 'lwc'; export default class MethodCaller extends LightningElement { handlePlay() { this.template.querySelector('c-video-player').play(); } }
我们定义了一个方法 handlePlay()
来触发该事件。然后使用 querySelector()
DOM 方法搜索名为 c-video-player 的 DOM 元素,并调用其公共方法。
在 HTML 中处理事件
因此,我们的选择器应用程序需要处理一种类型的事件——用户点击一个磁贴。当这种情况发生时,详细信息组件应该用相关磁贴的信息重新渲染。您可以用 HTML(在模板中添加事件监听器)或 JavaScript(编写事件监听器函数)处理事件。我们推荐使用 HTML 方法,如下所示。
每个磁贴组件都会监听用户的点击情况,因为磁贴组件的 HTML (tile.html) 包含一个 onclick
事件监听器。
<template> <div class="container"> <a onclick={tileClick}> <div class="title">{product.fields.Name.value}</div> <img class="product-img" src={product.fields.Picture_URL__c.value} alt={product.fields.Name.value}/> </a> </div> </template>
当用户点击 UI 中的一个磁贴实例时,onclick
监听器会调用 tile.js JavaScript 文件中的处理函数 tileClick
。
import { LightningElement, api } from 'lwc'; export default class Tile extends LightningElement { @api product; tileClick() { const event = new CustomEvent('tileclick', { // detail contains only primitives detail: this.product.fields.Id.value }); // Fire the event from c-tile this.dispatchEvent(event); } }
选择器应用程序的事件模式
在产品选择器应用程序中,我们使用了一个复杂的组件(包含若干父级组件和子级组件)。我们建议您通过组件的层次结构向上传播事件,以便父级组件可以响应子级事件。如果您有其他子级组件(并非触发事件的那一个),则可以将属性向下传递给这些子级组件以响应事件。
模式如下所示:
为此,我们需要将事件监听器和处理程序在层次结构中向上链接到 ebikes 组件。然后向下传递一个属性给详细信息组件。
在我们的文件中,您可以看到以下内容。
- tile.html 具有调用
tileClick
处理程序的onclick
事件监听器。
- Tile.js 具有
tileClick
方法,可以创建事件类型为tileclick
的新CustomEvent
和包含detail
值 (this.product.fields.Id.value
) 的对象。
- list.html 具有调用
handleTileClick
处理程序的ontileclick
监听器。
- List.js 具有
handleTileClick
方法,它传入事件 (evt
) 来创建另一个CustomEvent
(productselected
),带有同样包含detail
值evt.detail
的对象。同时在 JavaScript 中派发该事件:// Fire the event from c-list this.dispatchEvent(event);
- selector.html 具有调用
handleProductSelected
处理程序的onproductselected
事件监听器。
- Selector.js 具有
handleProductSelected
方法,它将selectedProductId
设置为传入的evt.detail
值。变量 "selectedProductId" 从选择器组件传递到 selector.htm 中的详细信息组件:product-id={selectedProductId}
。
- detail.html 有一个条件指令(还记得第 2 单元中的指令吗?)等待着产品值:
<template lwc:if={product}>
- detail.js 将这些部分结合起来。它会创建一个私有变量
_productId
来跟踪productId
值的状态。然后使用获取/设置模式来获取该值,并将其设置为变量product
(产品),让 detail.html 加载条件内容。
Getter 和 setter 是常见的 JavaScript 结构。它们使您能够为属性分配添加逻辑和条件。
import { LightningElement, api } from 'lwc'; import { bikes } from 'c/data'; export default class Detail extends LightningElement { product; // Private var to track @api productId _productId = undefined; // Use set and get to process the value every time it's // requested while switching between products set productId(value) { this._productId = value; this.product = bikes.find(bike => bike.fields.Id.value === value); } // getter for productId @api get productId(){ return this._productId; } }
每次点击磁贴时,该过程都会重复。
将文件部署到组织
让我们将这些新的 bikeCard 项目文件部署到您的组织中,看看它是如何工作的。部署新文件的步骤与上一单元相同,打开组织,在 Lightning 应用程序生成器中用这个应用程序创建一个页面。
- 在 VS Code bikeCard 项目中,右击 force-app/main/default 文件夹,并选择 SFDX: Deploy Source to Org(SFDX:将源部署到组织)。
- 在 VS Code 命令面板中,使用 SFDX: Open Default Org(SFDX:打开默认组织),打开您的组织。
- 使用选择器组件创建一个区域页面。
- 添加一个标签
Your Bike Selection
(您选择的自行车)。
- 将 selector(选择器)组件拖动到页面布局的顶部。
- 保存并为所有用户激活。
- 打开后可以看到组件已经出现在 UI 中。
至此,您就拥有了一个完全交互式的页面,由若干协同工作的组件组成。接下来,我们将尝试设置样式,并从组织获取实时数据。
资源
- Lightning Web 组件开发人员指南:Shadow DOM
- Lightning Web 组件开发人员指南:事件交流
- Lightning Web 组件开发人员指南:创建 Getter 和 Setter