Salesforce LWC学习 LDS & Wire Service 实现和后台数据交互 & meta xml配置
Posted zero.zhang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Salesforce LWC学习 LDS & Wire Service 实现和后台数据交互 & meta xml配置相关的知识,希望对你有一定的参考价值。
之前的几节都是基于前台变量进行相关的操作和学习,我们在项目中不可避免的需要获取数据以及进行DML操作。之前的内容中也有提到wire注解,今天就详细的介绍一下对数据进行查询以及DML操作以及Wire Service相关的知识。
一. LDS
学习LDS以前可以先看一下aura中LDS的概念salesforce lightning零基础学习(六)Lightning Data Service(LDS)。针对LWC中的LDS和aura中的功能原理很像,区别可能是语法和标签的区别。所以这里对LDS不做过多的描述,直接展开标签的用法。
LWC 封装了3个最基础的组件去和数据进行交互。分别是lightning-record-form / lightning-record-edit-form / lightning-record-view-form。和aura的用法也类似,lightning-record-form可以作为view/edit视图使用,lightning-record-edit-form针对edit视图使用并可以进行最大可能的自定义UI,lightning-record-view-form针对view视图使用并可以进行最大可能的自定义UI。
他们可以实现最基础的交互,如果他们标准功能满足不了,我们需要更加的自定义的功能,需要使用@wire 去指定LDS 的wire adapter。(封装在lightning/ui*Api中)
1. lightning-record-form
当我们只有ID,希望根据当前的用户显示当前用户对应的page layout布局的内容。我们便可以使用 lightning-record-form标签了,此标签遵循着FLS关系,用户只能看到自己可见的字段。此标签有三个模式:
view: 以output field展示,针对有权限编辑的字段,会显示编辑的按钮,当编辑某个值以后会显示save/cancel 按钮。
read-only:和上面区别为不显示可编辑按钮。
edit:以输入框进行展示,然后显示save/cancel按钮。
myComponentWithRecord 展示了 lightning:record-form的view的demo。
myComponentWithRecord.html:使用lightning-record-form展示account的detail数据,layout-type选择的是compact,mode为view。
1 <template> 2 <lightning-record-form 3 record-id={recordId} 4 object-api-name="Account" 5 layout-type="Compact" 6 mode="view"> 7 </lightning-record-form> 8 </template>
myComponentWithRecord.js:声明一个public的变量,名称固定为recordId。
1 // myComponent.js 2 import { LightningElement, api } from \'lwc\'; 3 export default class MyComponent extends LightningElement { 4 @api recordId; 5 }
myComponentWithRecord.js-meta.xml:配置当前component只允许用到record page中,并且只有account的record page可以选择此component。下面的内容我们会详细的讲解如何配置此xml文件,以及各个标签的作用。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="myComponentWithRecord"> 3 <apiVersion>46.0</apiVersion> 4 <isExposed>true</isExposed> 5 <targets> 6 <target>lightning__RecordPage</target> 7 </targets> 8 <targetConfigs> 9 <targetConfig targets="lightning__RecordPage"> 10 <property name="recordId" type="String"></property> 11 <objects> 12 <object>Account</object> 13 </objects> 14 </targetConfig> 15 </targetConfigs> 16 </LightningComponentBundle>
配置完以后我们只需要找到一个account,更改page layout或者app builder中针对account的lightning record page拖拽此component即可显示。
上面的demo中,我们在lightning-record-form中声明了一些简单的属性,除了上述的属性以外,此标签还有很多可选择的属性。所有属性如下:
- record-type-id: record type的ID,此属性用于当前的object有多个record type并且我们不想创建default的record type情况,这时我们需要传递 record type id;
- mode: view/edit/readonly。当我们没有指定ID的情况下,则这个类型默认是edit,即要创建一个object的记录;当有id情况下,默认是view。
- layout-type: Compact / Full. 用来指定当前的layout展现的形式。当我们新建记录时,即record id为空的情况下,layout-type只能渲染成Full.
- record-id: 需要展示/操作的记录ID,如果此属性为空,则代表要新建一条记录;
- object-api-name: 当前想要操作的object的API name,此属性是必填属性;
- columns: 表单中的列数,通常lightning:record-form不需要设置;
- fields: 如果我们不想通过layout-type展示,我们可以设置此fields选项,去按照我们的要求显示指定的字段信息。当然lightning:record-form不建议使用此属性,如果想要自定义显示字段,我们可以考虑用lightning:record-view-form以及lightning:record-edit-form去实现read/edit模式。
- density:设置label以及field在表单中的排列样式。有三个值: compact / comfy / auto.其中auto是default的值。
除了上述的属性以外,因为lightning-record-form有edit mode,所以它还拥有一些方法(以下仅用于edit mode,readonly不可用):
- load:当LDS加载数据完成后会调用此事件,此事件有一个返回的参数是detail,我们可以通过event.detail获取相关的内容;
- submit:当form表单提交了改变了的data时会自动触发此事件,此事件有一个可传入的参数fields,此参数用来指定要操作的字段集合;
- success:当form表单提交执行成功以后会自动触发此事件,此事件有一个返回的参数是detail;
- error:当form表单提交执行失败以后会自动触发此事件,返回的参数有detail;
- cancel:当form表单没有提交点击cancel以后会自动触发此事件。
这几个事件在某种情况下还是有一定联系的。
- 当我们执行submit事件以后,在没有错误的情况下,会先执行load事件,执行成功以后会执行success事件,当执行完success事件以后会再一次load事件。 submit -> load -> success -> load。
- 当我们执行submit事件以后,如果有错误会执行error事件。 submit -> error。
- 当我们执行cancel事件以后,将会执行load事件。cancel -> load。
- 当我们执行完cancel事件以后,页面的cancel/submit按钮会隐藏,可编辑字段会展示编辑的图标,当我们对某个字段进行编辑时,会执行load事件。
下面通过一个demo更好的了解edit功能。
myComponentWithRecordEdit.html:展示一个edit模式的lightning-record-form,并针对这些标准的事件设置相关的handler。
1 <template> 2 <lightning-record-form 3 record-id={recordId} 4 object-api-name={objectApiName} 5 fields={fields} 6 columns="2" 7 mode="edit" 8 onsubmit={handleSubmit} 9 onsuccess={handleSuccess} 10 onerror={handleError} 11 oncancel={handleCancel} 12 onload={handleLoad} 13 > 14 </lightning-record-form> 15 </template>
myComponentWithRecordEdit.js:设置相关的handler逻辑,头部我们可以看到import salesforce/lightning相关的内容。这个我们在后续会以refrence内容详细说明。这里还有event.preventDefault()方法。当我们捕获submit 事件并以编程方式提交表单,这种情况我们需要使用event.preventDefault方法去取消事件的默认行为,否则会进行重复的表单提交。
1 /* eslint-disable no-console */ 2 import { LightningElement, api } from \'lwc\'; 3 import { ShowToastEvent } from \'lightning/platformShowToastEvent\'; 4 5 import NAME_FIELD from \'@salesforce/schema/Account.Name\'; 6 import REVENUE_FIELD from \'@salesforce/schema/Account.AnnualRevenue\'; 7 import INDUSTRY_FIELD from \'@salesforce/schema/Account.Industry\'; 8 9 export default class MyComponentWithRecordEdit extends LightningElement { 10 // The record page provides recordId and objectApiName 11 @api recordId; 12 @api objectApiName; 13 14 fields = [NAME_FIELD, REVENUE_FIELD, INDUSTRY_FIELD]; 15 16 handleLoad(event) { 17 console.log(\'execute handle load\'); 18 } 19 20 handleSubmit(event){ 21 console.log(\'execute handle submit\'); 22 event.preventDefault(); // stop the form from submitting 23 const fields = event.detail.fields; 24 fields.LastName = \'My Custom Last Name\'; // modify a field 25 this.template.querySelector(\'lightning-record-form\').submit(fields); 26 } 27 28 handleSuccess(event) { 29 console.log(\'execute handle success\'); 30 const evt = new ShowToastEvent({ 31 title: "Account Operated", 32 message: "Record ID: " + event.detail.id, 33 variant: "success" 34 }); 35 this.dispatchEvent(evt); 36 } 37 38 handleError(event) { 39 console.log(\'execute handle error\'); 40 const evt = new ShowToastEvent({ 41 title: "Account Operated", 42 message: event.detail.message, 43 variant: "error" 44 }); 45 this.dispatchEvent(evt); 46 } 47 48 handleCancel(event) { 49 console.log(\'execute handle cancel\') 50 const evt = new ShowToastEvent({ 51 title: "Account canceled", 52 variant: "cancel" 53 }); 54 this.dispatchEvent(evt); 55 } 56 }
效果展示:
1) 点击Save更新数据操作
2) 当Save后有error情况
2. lightning-record-view-form
lightning-record-form功能确实比较好用,但是如果用户想要显示指定的字段并且希望字段以指定的顺序进行显示只读的pagelayout时,使用lightning-record-form便无法实现了,这个时候我们需要使用lightning-record-view-form搭配lightning-output-field用来实现按照自己的要求展示一个或者多个字段。显示时,我们通常搭配grid一起使用按需展现多行多列效果。grid使用可以参考:https://www.lightningdesignsystem.com/utilities/grid/
此组件有以下的属性可供选择:
- record-id:当前要显示记录的记录ID,此字段必填;
- object-api-name: 当前object的API 名称,此字段必填;
- density:设置label以及field在表单中的排列样式。
除上述属性以外,lightning-record-view-form支持load事件,可用参数为data,存储的是记录的数据。详见上面的demo。下面的demo为使用此标签实现只读的数据。
myComponentWithRecordView.html:通过引入lightning-record-view-form,然后配合lightning-output-field展示信息,这里展示的是一行四列的内容布局。
1 <template> 2 <lightning-record-view-form 3 record-id={recordId} 4 object-api-name="Account"> 5 <div class="slds-grid"> 6 <div class="slds-col"> 7 <lightning-output-field field-name="Name"></lightning-output-field> 8 </div> 9 <div class="slds-col"> 10 <lightning-output-field field-name="Phone"></lightning-output-field> 11 </div> 12 <div class="slds-col"> 13 <lightning-output-field field-name="AnnualRevenue"></lightning-output-field> 14 </div> 15 <div class="slds-col"> 16 <lightning-output-field field-name="Industry"></lightning-output-field> 17 18 </div> 19 </div> 20 </lightning-record-view-form> 21 </template>
myComponentWithRecordView.js
1 import { LightningElement, api } from \'lwc\'; 2 3 export default class MyComponentWithRecordView extends LightningElement { 4 @api recordId; 5 }
显示效果:
3. lightning-record-edit-form
lightning-record-form可以实现create/edit功能,和view的情况一样,当用户想要深度自定义时,lightning-record-form显然达不到需求,这个时候我们就需要 lightning-record-edit-form。
此组件通常和lightning-input-field一起用,用来显示需要编辑的字段。我们针对布局中偶尔可能需要显示只读字段,我们可以使用lightning-output-field以及lightning-formatted-name一起搭配使用。
此组件支持的方法和lightning-record-form基于edit模式下差不太多,同lightning-record-form一样,如果想要创建记录,只需要record-id为空即可。
lightning-record-form与lightning-record-edit-form使用不同的地方可以整理两点:
1) lightning-record-form有cancel事件,lightning-record-edit-form没有,需要使用lightning-button展示。针对cancel按钮的默认处理方式也不同,针对lightning-record-form点击cancel以后会默认恢复view的状态,针对lightning-record-edit-form不可以,我们需要针对字段调用reset方法才可以;
2) lightning-record-edit-form 需要使用lightning-button并且type为submit才可以正常提交表单,lightning-record-form不需要。
下面通过一个demo进行更好的了解。
recordEditFormSample.html
1 <template> 2 <lightning-record-edit-form 3 record-id={recordId} 4 object-api-name={objectApiName} 5 onsubmit={handleSubmit} 6 onload={handleLoad} 7 onsuccess={handleSuccess} 8 onerror={handleError} 9 > 10 <lightning-messages></lightning-messages> 11 <lightning-input-field field-name="Name"></lightning-input-field> 12 <lightning-input-field field-name="Industry"></lightning-input-field> 13 <lightning-input-field field-name="AnnualRevenue"></lightning-input-field> 14 <div class="slds-m-top_medium"> 15 <lightning-button class="slds-m-top_small" label="Cancel" onclick={handleReset}></lightning-button> 16 <lightning-button class="slds-m-top_small" type="submit" label="Save Record"></lightning-button> 17 </div> 18 </lightning-record-edit-form> 19 </template>
recordEditFormSample.js
1 /* eslint-disable no-console */ 2 import { LightningElement, api } from \'lwc\'; 3 import { ShowToastEvent } from \'lightning/platformShowToastEvent\'; 4 export default class RecordEditFormSample extends LightningElement { 5 6 @api recordId; 7 @api objectApiName; 8 9 handleSubmit(event) { 10 event.preventDefault(); // stop the form from submitting 11 const fields = event.detail.fields; 12 if(fields.Industry === null || fields.Industry === \'\') { 13 const evt = new ShowToastEvent({ 14 title: "Account Operated Failed", 15 message: "Account Industry cannot be blank", 16 variant: "error" 17 }); 18 this.dispatchEvent(evt); 19 return; 20 } 21 this.template.querySelector(\'lightning-record-edit-form\').submit(fields); 22 } 23 24 handleLoad(event) { 25 console.log(\'execute load\'); 26 } 27 28 handleSuccess(event) { 29 const evt = new ShowToastEvent({ 30 title: "Account Operated Success", 31 message: "Record is :" + event.detail.id, 32 variant: "success" 33 }); 34 this.dispatchEvent(evt); 35 } 36 37 handleError(event) { 38 const evt = new ShowToastEvent({ 39 title: "Account Operated Failed", 40 message: event.detail.message, 41 variant: "error" 42 }); 43 this.dispatchEvent(evt); 44 } 45 46 handleReset(event) { 47 const inputFields = this.template.querySelectorAll( 48 \'lightning-input-field\' 49 ); 50 if (inputFields) { 51 inputFields.forEach(field => { 52 field.reset(); 53 }); 54 } 55 } 56 }
效果展示
1) 包含错误的情况展示
2. 正常保存的展示
二. Wire Service
从上面内容可以看到,LDS已经很强大了,但是针对LDS处理不了的情况呢,比如获取父表信息,对数据中的内容进行格式化处理,这些可能标准功能很难达到或者达不到,这种我们便需要wire adapter去对LDS进行增强。
1. 使用封装的函数进行LDS增强
我们在组件中使用@wire标签在javascript中去获取数据,这些数据由lightning/ui*Api 模块的一个wire adapter获取。
wire adapter有很多强大的功能,比如getRecord / getFieldValue(record, field)。 我们在代码中经常会看到 import salesforce/xxx 以及 import lightning/ui*Api/xxx,我们会在下一节LWC博客中详细的讲解。
我们称wire service在某种程度上是reactive的,原因是它提供了一个reactive的变量,我们使用$符号声明在变量前面,当这个变量改变以后,wire service将会获取一个新的版本的数据,从而重新渲染component。
我们基于三个步骤使用wire service。
1 import { adapterId } from \'adapterModule\'; 2 @wire(adapterId, adapterConfig) 3 propertyOrFunction;
adapterId: wire adapter的标识符。比如我们需要用到lightning/uiRecordApi中的getRecord,那getRecord为这里的adapterId;
adapterModule:当前这个标识符封装在哪个适配器模块中,lwc封装了好多的wire adapter 标识符,他们在以下的adapterModule中:lightning/uiListApi / lightning:uiObjectInfoApi / lightning:uiRecordApi
adapterConfig:针对wire adapter特定的配置对象信息。配置对象的属性值可以是字符串,也可以通过@salesforce/schema方式引入的表和字段信息。salesforce比较推荐后者;
propertyOrFunction:一个私有变量或者方法,这个变量或者方法从wire service通过配置信息获取到最终的数据。如果这个是一个变量声明了wire,返回的结果为data property以及error property,如果这个是一个方法声明了wire,这个方法返回的结果包含data property 以及error property。
概念看起来比较绕,通过一个demo(getRecord)便可以更好的了解。
https://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.reference_wire_adapters_record
getRecord wire adapter在lightning/uiRecordApi模块下,所以第一个步骤确定为:
import {getRecord} from \'lightning/uiRecordApi\'
adapterConfig 针对getRecord有两个可以的配置项。
1) { recordId: string, fields: string[], optionalFields?: string[]}
2){ recordId: string, layoutTypes: string[], modes?: string[], optionalFields?: string[]}
- recordId代表想要获取的记录的ID
- fields / layoutTypes这两个必须要有一个存在。fields代表当前想要查询的字段,官方建议我们使用@salesforce/schema方式获取相关的metadata信息。这里需要注意的是,如果当前的上下文用户没有针对字段的访问权限,将会报错。
- layoutTypes: compact/full这两个取值,因为pagelayout可能会有改变,所以针对要求固定字段的情况下,使用上述fields方式。如果要跟随page layout方式,可以选择此方式
- modes: 当选择了layoutTypes以后,我们可以选择modes,可选择的值为Create/Edit/View。
- optionalFields和fields功能类似,区别为如果引入一个上下文用户没有访问权限的字段,使用此参数不会报错,没有权限的字段对应的值不会返回而已。
demo用于获取account的name并对name进行每个字母都大写处理:
RecordViewFormWithWireService.js:使用wire adapter中的getRecord获取相关metadata的value,然后进行format处理。salesforce建议我们获取metadata命名采用object_field_name方式,当然这个是规范,不是规则。这里之所以对wiredAccount获取值部分添加data不为undefined原因是当我们加载数据时,最开始wiredAccount为{},所以get accountName方法会挂掉提示undefined信息,取Account Name值有两种方式,一种是通过各种点的方式取到,另一个是通过wire service封装的getFieldValue方法获取。
1 /* eslint-disable no-console */ 2 import { LightningElement,wire,api,track } from \'lwc\'; 3 import { getRecord,getFieldValue } from \'lightning/uiRecordApi\'; 4 import ACCOUNT_NAME_FIELD from \'@salesforce/schema/Account.Name\'; 5 import ACCOUNT_INDUSTRY_FIELD from \'@salesforce/schema/Account.Industry\'; 6 import ACCOUNT_ANNUAL_REVENUE from \'@salesforce/schema/Account.AnnualRevenue\'; 7 const ACCOUNT_FIELDS = [ACCOUNT_NAME_FIELD,ACCOUNT_INDUSTRY_FIELD,ACCOUNT_ANNUAL_REVENUE]; 8 export default class RecordViewFormWithWireService extends LightningElement { 9 @api recordId; 10 11 @wire(getRecord,{recordId:\'$recordId\',fields:ACCOUNT_FIELDS}) 12 wiredAccount; 13 14 get accountName() { 15 // console.log(JSON.stringify(this.wiredAccount)); 16 // console.log(\'xx\'); 17 // if(this.wiredAccount.data !== undefined) { 18 // return this.wiredAccount.data.fields.Name.value.toUpperCase(); 19 // } 20 // return \'\'; 21 return this.wiredAccount.data != undefined ? getFieldValue(this.wiredAccount.data,ACCOUNT_NAME_FIELD).toUpperCase() : \'\'; 22 } 23 }
recordViewFormWithWireService.html
1 <template> 2 {accountName} 3 </template>
结果展示:
上面的wire service中只简单的描述了getRecord的用法以及顺带描述了getFieldValue,LWC提供了很多的wire adapter function,下一节的博客会有详细的说明。当我们使用了wire adapter增强LDS以后,可以做到更强的功能,比如获取父对象字段值,进行字段值的format。但是我们想要更复杂的操作,比如对数据进行filter,获取子数据信息,那我们就得需要访问apex获取数据了。下面内容为通过apex获取数据。
2. 和后台apex方法交互
有两种方式可以调用apex方法,一种是wire方式直接调用,另外一种通过指定的命令方式。下面对这两种方式进行简单的介绍。
两种方式的第一步均需要引入这个需要调用的apex
1 import apexMethodName from \'@salesforce/apex/Namespace.Classname.apexMethodReference\';
apexMethodName—用来识别引入的apex方法的名称。这个名称通常和方法名称相同并且后期引用均使用此名称。
apexMethodReference—引用的apex中的method的名称
Classname—当前的method所在的apex class的名称
Namespace—当前的namespace,默认为c.如果是c的情况下可以自动省略
比如我们在 ContactController中有一个方法名字是getContactList,则我们的apexMethodName默认应该命名为 getContactList,
apexMethodRefernce为getContactList,Classname为ContactController,Namespace为c。如下所示:
import getContactList from \'@salesforce/apex/ContactController.getContactList\';
我们在aura项目中,如果js中调用apex中的方法要求当前的方法声明为@AuraEnabled,同样使用LWC也要求后台的apex方法需要声明为@AuraEnabled,并且方法要求static & (public / global)
当我们针对的是获取数据,没有DML操作情况下,我们可以声明方法为cacheable=true去提升客户端的性能。如果当前的数据存在DML操作,不是纯粹的取数据,则不应该声明cacheable=true。
我们针对和apex交互的两种方式,使用wire方式必须要指定后台的apex方法声明 cacheable=true,使用命令方式则不需要有这个限制。当然,如果我们使用了cacheable声明以后,当我们觉得数据可能不是最新或者是有问题的数据情况下,我们可以调用refreshApex()去获取最新的数据。
如果我们apex中涉及到和外部系统的长时间的交互,我们可以对方法声明 continuation=true,如果同时声明了 cacheable以及continuation,则中间使用空格分隔。如下所示:
@AuraEnabled(continuation=true cacheable=true)
后台方法的要求已经说完了,下面介绍两种方式的调用。
1)wire方式调用:
@wire(apexMethod, { apexMethodParams })
propertyOrFunction;
apexMethod: 和上面import的apexMethodName相同;
apexMethodParams:apexMethodName传递的参数。后台的方法可以无参数和有参数,如果无参数将apexMethodParams设置为null,如果有参数则传递此参数。这里需要注意的是,如果apexMethodParams设置为null可以正常调用,意思是无参方法,如果此参数为undefined,则wire不会调用后台的此方法。
propertyOrFunction:wire装载给变量或者方法。如果是变量,后台方法如果没有错误情况下,返回的是正常的返回内容。否则返回的是error变量。
如果是方法,则方法对应的是一个object,object中包含了data变量或者error变量。说起来比较绕,通过一个例子更好的了解。
下面的例子为wire装载给方法。wiredContacts返回变量中有两个参数,error,data。我们通常判断如果data不为空,则正常返回。如果error不为空,则代表当前的数据获取存在异常。demo中没有形参,如果想要有形参,在getContactList方法后面使用逗号分隔以后添加形参
1 @wire(getContactList) 2 wiredContacts({ error, data }) { 3 if (data) { 4 this.contacts = data; 5 this.error = undefined; 6 } else if (error) { 7 this.error = error; 8 this.contacts = undefined; 9 } 10 }
下面的例子为wire装载给变量。findContacts有一个参数searchKey。我们使用$符号代表当前的变量是动态的reactive的,返回值给contacts。如果正常返回,contacts里面是后台的apex 返回的数据列表。如果存在error,contacts里面是error变量。
1 @wire(findContacts, { searchKey: \'$searchKey\' }) 2 contacts;
因为后台声明了cacheable以后,只有刷新以后才能重新装载最新版本的数据。LWC针对wire声明的变量提供了refreshApex方法。使用两步走。
1. import { refreshApex } from \'@salesforce/apex\'; 2. refreshApex(wiredProperty)
其中wiredProperty为我们使用wire标签声明的变量。这里需要注意的一点是,针对wire声明的方法无法使用此方法进行刷新缓存操作。
如果声明了方法我们想清空缓存,需要先声明变量。然后方法中对此变量赋值,然后再refreshApex中传递声明的变量。
下面以一个例子更好的了解wire方式调用apex代码以及refreshApex的使用。
ContactController中声明了一个方法findContacts,形参是一个string用来传递想要搜索的contact的name。此方法使用AuraEnabled并且指定了cacheable=true,则LWC针对前台处理可以使用wire方式,也可以使用命令方式。
1 public with sharing class ContactController { 2 @AuraEnabled(cacheable=true) 3 public static List<Contact> findContacts(String searchKey) { 4 String key = \'%\' + searchKey + \'%\'; 5 return [SELECT Id, Name, Title, Phone, Email FROM Contact WHERE Name LIKE :key LIMIT 10]; 6 } 7 }
contactListSearchWithWireService.html:展示搜索出来的contact的信息,上面有一个输入框以及两个按钮,点击search进行搜索,点击refresh清空缓存重新获取。
1 <template> 2 Salesforce LWC学习(十八) datatable展示 imageSalesforce LWC学习 Dependence Picklist实现
Salesforce LWC学习(十六) Validity 在form中的使用浅谈