Salesforce LWC学习(三十) lwc superbadge项目实现

2023-02-13,,,

本篇参考:https://trailhead.salesforce.com/content/learn/superbadges/superbadge_lwc_specialist

我们做lwc的学习时,因为很多人可能还没接触过lwc的项目,所以通过学习知道很多的知识点,但是可能没有机会做到一个小项目,salesforce lwc superbadge正好可以在将知识点串起来基础上,深化学习,当一个小项目练手。首先先按照上方的superbadge要求,安装一个unlocked package,然后导入到基础数据。导入以后数据以及表和基本的 component的壳子就都有了。一步一步进行分析。观看以下视频查看效果:https://www.iqiyi.com/v_1rz2ad7afb8.html

(注: 如果没有lwc的基础,请务必先了解基础以后查看,否则看此篇纯属浪费时间)

一. 表结构

demo中主要用到了三个表。

Boat:存储了船的一些基础信息;
BoatReview:船的评价信息;
Boat Type:存储船的类型。

 二. component层级结构

demo中包含了以下的 component信息,其中层级结构如下所示。我们可以看到 boatSearch / boatDetailTabs以及boatMap是同层,他们传递 recordId的方式便需要通过 Lightning Message Service方式。

 三. 涉及到技术以及代码详情

涉及到的主要技术

1. lightning message service:用于没有关联的组件间的信息传播,类似于aura中的 application event,实现跨组件传递 参数,可以参考此篇文章:

Salesforce LWC学习(二十三) Lightning Message Service 浅谈 ;

2. 父子component传值,子如何创建事件,父如何去调度事件,可以参考此篇文章:

Salesforce LWC学习(四) 父子component交互 / component声明周期管理 / 事件处理;

3. 通过 wire service或者Lightning Data Service实现和数据的交互,可以参考此篇文章:

Salesforce LWC学习(五) LDS & Wire Service 实现和后台数据交互 & meta xml配置;

4. Navigation 以及Toast实现展示Toast信息以及页面跳转功能,可以参考此篇文章:

Salesforce LWC学习(七) Navigation & Toast;

5. lwc提供的各种预置的组件,可以参考官方链接:https://developer.salesforce.com/docs/component-library/overview/components;

6. wire adapter等知识点使用。可以参考此篇文章:

Salesforce LWC学习(六) @salesforce & lightning/ui*Api Reference

预备工作,按照1操作中的步骤创建Message Channel:在messageChannels目录下创建一个 BoatMessageChannel.messageChannel-meta.xml的文件,里面包含主要字段为 fieldName,可以在上方 superbadge去进行适当的copy。

代码如下:BoatDataService.cls:包含了项目中用到的后台需要使用的所有的方法

  1 public with sharing class BoatDataService {
2
3 public static final String LENGTH_TYPE = 'Length';
4 public static final String PRICE_TYPE = 'Price';
5 public static final String TYPE_TYPE = 'Type';
6 @AuraEnabled(cacheable=true)
7 public static List<Boat__c> getBoats(String boatTypeId) {
8 // Without an explicit boatTypeId, the full list is desired
9 String query = 'SELECT '
10 + 'Name, Description__c, Geolocation__Latitude__s, '
11 + 'Geolocation__Longitude__s, Picture__c, Contact__r.Name, '
12 + 'BoatType__c, BoatType__r.Name, Length__c, Price__c '
13 + 'FROM Boat__c';
14 if (String.isNotBlank(boatTypeId)) {
15 query += ' WHERE BoatType__c = :boatTypeId';
16 }
17 query += ' WITH SECURITY_ENFORCED ';
18 return Database.query(query);
19 }
20
21 @AuraEnabled(cacheable=true)
22 public static List<Boat__c> getSimilarBoats(Id boatId, String similarBy) {
23 List<Boat__c> similarBoats = new List<Boat__c>();
24 List<Boat__c> parentBoat = [SELECT Id, Length__c, Price__c, BoatType__c, BoatType__r.Name
25 FROM Boat__c
26 WHERE Id = :boatId
27 WITH SECURITY_ENFORCED];
28 if (parentBoat.isEmpty()) {
29 return similarBoats;
30 }
31 if (similarBy == LENGTH_TYPE) {
32 similarBoats = [
33 SELECT Id, Contact__r.Name, Name, BoatType__c, BoatType__r.Name, Length__c, Picture__c, Price__c, Year_Built__c
34 FROM Boat__c
35 WHERE Id != :parentBoat.get(0).Id
36 AND (Length__c >= :parentBoat.get(0).Length__c / 1.2)
37 AND (Length__c <= :parentBoat.get(0).Length__c * 1.2)
38 WITH SECURITY_ENFORCED
39 ORDER BY Length__c, Price__c, Year_Built__c
40 ];
41 } else if (similarBy == PRICE_TYPE) {
42 similarBoats = [
43 SELECT Id, Contact__r.Name, Name, BoatType__c, BoatType__r.Name, Length__c, Picture__c, Price__c, Year_Built__c
44 FROM Boat__c
45 WHERE Id != :parentBoat.get(0).Id
46 AND (Price__c >= :parentBoat.get(0).Price__c / 1.2)
47 AND (Price__c <= :parentBoat.get(0).Price__c * 1.2)
48 WITH SECURITY_ENFORCED
49 ORDER BY Price__c, Length__c, Year_Built__c
50 ];
51 } else if (similarBy == TYPE_TYPE) {
52 similarBoats = [
53 SELECT Id, Contact__r.Name, Name, BoatType__c, BoatType__r.Name, Length__c, Picture__c, Price__c, Year_Built__c
54 FROM Boat__c
55 WHERE Id != :parentBoat.get(0).Id
56 AND (BoatType__c = :parentBoat.get(0).BoatType__c)
57 WITH SECURITY_ENFORCED
58 ORDER BY Price__c, Length__c, Year_Built__c
59 ];
60 }
61 return similarBoats;
62 }
63
64 @AuraEnabled(cacheable=true)
65 public static List<BoatType__c> getBoatTypes() {
66 return [SELECT Name, Id FROM BoatType__c WITH SECURITY_ENFORCED ORDER BY Name];
67 }
68
69 @AuraEnabled(cacheable=false)
70 public static List<BoatReview__c> getAllReviews(Id boatId) {
71 return [
72 SELECT
73 Id,
74 Name,
75 Comment__c,
76 Rating__c,
77 LastModifiedDate,
78 CreatedDate,
79 CreatedBy.Name,
80 CreatedBy.SmallPhotoUrl,
81 CreatedBy.CompanyName
82 FROM
83 BoatReview__c
84 WHERE
85 Boat__c =:boatId
86 WITH SECURITY_ENFORCED
87 ORDER BY
88 CreatedDate DESC
89 ];
90 }
91
92 @AuraEnabled(cacheable=true)
93 public static String getBoatsByLocation(Decimal latitude, Decimal longitude, String boatTypeId) {
94 // Without an explicit boatTypeId, the full list is desired
95 String query = 'SELECT Name, Geolocation__Latitude__s, Geolocation__Longitude__s FROM Boat__c ';
96 if (String.isNotBlank(boatTypeId)) {
97 query += 'WHERE BoatType__c = :boatTypeId ';
98 }
99 query += ' WITH SECURITY_ENFORCED ORDER BY DISTANCE(Geolocation__c, GEOLOCATION(:latitude, :longitude), \'mi\') LIMIT 10';
100 return JSON.serialize(Database.query(query));
101 }
102 }

fiveStartRating.js:使用了第三方的js以及css;

 1 //import fivestar static resource, call it fivestar
2 import { LightningElement, api } from 'lwc';
3 import fivestar from '@salesforce/resourceUrl/fivestar';
4 import { ShowToastEvent } from 'lightning/platformShowToastEvent';
5 import { loadStyle, loadScript } from 'lightning/platformResourceLoader';
6
7 const ERROR_TITLE = 'Error loading five-star';
8 const ERROR_VARIANT = 'error';
9 const EDITABLE_CLASS = 'c-rating';
10 const READ_ONLY_CLASS = 'readonly c-rating';
11
12 export default class FiveStarRating extends LightningElement {
13 //initialize public readOnly and value properties
14 @api readOnly;
15 @api value;
16
17 editedValue;
18 isRendered;
19
20 //getter function that returns the correct class depending on if it is readonly
21 get starClass() {
22 return this.readOnly ? READ_ONLY_CLASS : EDITABLE_CLASS;
23 }
24
25 // Render callback to load the script once the component renders.
26 renderedCallback() {
27 if (this.isRendered) {
28 return;
29 }
30 this.loadScript();
31 this.isRendered = true;
32 }
33
34 //Method to load the 3rd party script and initialize the rating.
35 //call the initializeRating function after scripts are loaded
36 //display a toast with error message if there is an error loading script
37 loadScript() {
38 Promise.all([
39 loadStyle(this, fivestar + '/rating.css'),
40 loadScript(this, fivestar + '/rating.js')
41 ]).then(() => {
42 this.initializeRating();
43 }).catch(()=>{
44 const event = new ShowToastEvent({title:ERROR_TITLE, variant:ERROR_VARIANT});
45 this.dispatchEvent(event);
46 });
47 }
48
49 initializeRating() {
50 let domEl = this.template.querySelector('ul');
51 let maxRating = 5;
52 let self = this;
53 let callback = function (rating) {
54 self.editedValue = rating;
55 self.ratingChanged(rating);
56 };
57 this.ratingObj = window.rating(
58 domEl,
59 this.value,
60 maxRating,
61 callback,
62 this.readOnly
63 );
64 }
65
66 // Method to fire event called ratingchange with the following parameter:
67 // {detail: { rating: CURRENT_RATING }}); when the user selects a rating
68 ratingChanged(rating) {
69 this.dispatchEvent(new CustomEvent('ratingchange', {detail: { rating: rating }}));
70 }
71 }

fiveStartRating.html: 展示UI

<template>
<ul class={starClass}></ul>
</template>

boatSearchForm.js: wire adapter和后台交互以及注册自定义事件

 1 import { LightningElement, wire, track } from 'lwc';
2 import getBoatTypes from '@salesforce/apex/BoatDataService.getBoatTypes';
3 export default class BoatSearchForm extends LightningElement {
4 selectedBoatTypeId = '';
5
6 // Private
7 error = undefined;
8
9 // Needs explicit track due to nested data
10 @track searchOptions = [];
11
12 // Wire a custom Apex method
13 @wire(getBoatTypes)
14 boatTypes({ error, data }) {
15 if (data) {
16 this.searchOptions = data.map(type => ({
17 // TODO: complete the logic
18 label: type.Name,value: type.Id
19 }));
20 this.searchOptions.unshift({ label: 'All Types', value: '' });
21 } else if (error) {
22 this.searchOptions = undefined;
23 this.error = error;
24 }
25 }
26
27 // Fires event that the search option has changed.
28 // passes boatTypeId (value of this.selectedBoatTypeId) in the detail
29 handleSearchOptionChange(event) {
30 this.selectedBoatTypeId = event.detail.value.trim();
31 const searchEvent = new CustomEvent('search', {detail:{boatTypeId: this.selectedBoatTypeId} });
32 this.dispatchEvent(searchEvent);
33 }
34 }

boatSearchForm.html:搜索UI展示

<template>
<lightning-layout>
<lightning-layout-item class="slds-align-middle">
<lightning-combobox class="slds-align-middle" options={searchOptions} onchange={handleSearchOptionChange} value={selectedBoatTypeId}></lightning-combobox>
</lightning-layout-item>
</lightning-layout>
</template>

boatTile.js: 获取父组件传递过来的内容,注册自定义事件

 1 import { LightningElement, api} from "lwc";
2 const TILE_WRAPPER_SELECTED_CLASS = "tile-wrapper selected";
3 const TILE_WRAPPER_UNSELECTED_CLASS = "tile-wrapper";
4 export default class BoatTile extends LightningElement {
5 @api boat;
6 @api selectedBoatId;
7 get backgroundStyle() {
8 return `background-image:url(${this.boat.Picture__c})`;
9 }
10 get tileClass() {
11 return this.selectedBoatId == this.boat.Id ? TILE_WRAPPER_SELECTED_CLASS : TILE_WRAPPER_UNSELECTED_CLASS;
12 }
13 selectBoat() {
14 this.selectedBoatId = !this.selectedBoatId;
15 const boatselect = new CustomEvent("boatselect", {
16 detail: {
17 boatId: this.boat.Id
18 }
19 });
20 this.dispatchEvent(boatselect);
21 }
22 }

boatTile.html:详情页UI展示

<template>
<div onclick={selectBoat} class={tileClass}>
<div style={backgroundStyle} class="tile"></div>
<div class="lower-third">
<h1 class="slds-truncate slds-text-heading_medium">{boat.Name}</h1>
<h2 class="slds-truncate slds-text-heading_small">{boat.Contact__r.Name}</h2>
<div class="slds-text-body_small">
Price: <lightning-formatted-number maximum-fraction-digits="2" format-style="currency" currency-code="USD" value={boat.Price__c}> </lightning-formatted-number>
</div>
<div class="slds-text-body_small"> Length: {boat.Length__c} </div>
<div class="slds-text-body_small"> Type: {boat.BoatType__r.Name} </div>
</div>
</div>
</template>

boatTile.css

.tile {
width: 100%;
height: 220px;
padding: 1px !important;
background-position: center;
background-size: cover;
border-radius: 5px;
}
.selected {
border: 3px solid rgb(0, 95, 178);
border-radius: 5px;
}
.tile-wrapper {
cursor: pointer;
padding: 5px;
}

boatsNearMe.js: wire adapter / toast / 生命周期函数/ lightning-map 预置函数

 1 import { LightningElement, track, wire, api } from 'lwc';
2 import getBoatsByLocation from '@salesforce/apex/BoatDataService.getBoatsByLocation';
3 import { ShowToastEvent } from 'lightning/platformShowToastEvent';
4
5 const LABEL_YOU_ARE_HERE = 'You are here!';
6 const ICON_STANDARD_USER = 'standard:user';
7 const ERROR_TITLE = 'Error loading Boats Near Me';
8 const ERROR_VARIANT = 'error';
9 export default class BoatsNearMe extends LightningElement {
10 @api boatTypeId;
11 @track mapMarkers = [];
12 @track isLoading = true;
13 @track isRendered = false;
14 latitude;
15 longitude;
16 @wire(getBoatsByLocation, { latitude: '$latitude', longitude: '$longitude', boatTypeId: '$boatTypeId' })
17 wiredBoatsJSON({ error, data }) {
18 if (data) {
19 this.createMapMarkers(data);
20 } else if (error) {
21 this.dispatchEvent(
22 new ShowToastEvent({
23 title: ERROR_TITLE,
24 message: error.body.message,
25 variant: ERROR_VARIANT
26 })
27 );
28 this.isLoading = false;
29 }
30 }
31 renderedCallback() {
32 if (this.isRendered == false) {
33 this.getLocationFromBrowser();
34 }
35 this.isRendered = true;
36 }
37 getLocationFromBrowser() {
38 navigator.geolocation.getCurrentPosition(
39 (position) => {
40 this.latitude = position.coords.latitude;
41 this.longitude = position.coords.longitude;
42 },
43 (e) => {
44
45 }, {
46 enableHighAccuracy: true
47 }
48 );
49 }
50 createMapMarkers(boatData) {
51 this.mapMarkers = boatData.map(rowBoat => {
52 return {
53 location: {
54 Latitude: rowBoat.Geolocation__Latitude__s,
55 Longitude: rowBoat.Geolocation__Longitude__s
56 },
57 title: rowBoat.Name,
58 };
59 });
60 this.mapMarkers.unshift({
61 location: {
62 Latitude: this.latitude,
63 Longitude: this.longitude
64 },
65 title: LABEL_YOU_ARE_HERE,
66 icon: ICON_STANDARD_USER
67 });
68 this.isLoading = false;
69 }
70 }

boatsNearMe.html

<template>
<lightning-card class="slds-is-relative">
<!-- The template and lightning-spinner goes here -->
<template if:true={isLoading}>
<lightning-spinner
alternative-text="Loading" variant="brand">
</lightning-spinner>
</template>
<!-- The lightning-map goes here -->
<lightning-map map-markers={mapMarkers}></lightning-map>
<div slot="footer">Top 10 Only!</div>
</lightning-card>
</template>

boatSearchResults.js: wire adapter / lightning message service / toast / 父组件处理子组件事件

  1 import { LightningElement, wire, api, track } from 'lwc';
2 import getBoats from '@salesforce/apex/BoatDataService.getBoats';
3 import { updateRecord } from 'lightning/uiRecordApi';
4 import { ShowToastEvent } from 'lightning/platformShowToastEvent';
5 import { refreshApex } from '@salesforce/apex';
6 import { publish, MessageContext } from 'lightning/messageService';
7 import BoatMC from '@salesforce/messageChannel/BoatMessageChannel__c';
8
9 export default class BoatSearchResults extends LightningElement {
10 boatTypeId = '';
11 @track boats;
12 @track draftValues = [];
13 selectedBoatId = '';
14 isLoading = false;
15 error = undefined;
16 wiredBoatsResult;
17
18 @wire(MessageContext) messageContext;
19
20 columns = [
21 { label: 'Name', fieldName: 'Name', type: 'text', editable: 'true' },
22 { label: 'Length', fieldName: 'Length__c', type: 'number', editable: 'true' },
23 { label: 'Price', fieldName: 'Price__c', type: 'currency', editable: 'true' },
24 { label: 'Description', fieldName: 'Description__c', type: 'text', editable: 'true' }
25 ];
26
27 @api
28 searchBoats(boatTypeId) {
29 this.isLoading = true;
30 this.notifyLoading(this.isLoading);
31 this.boatTypeId = boatTypeId;
32 }
33
34 @wire(getBoats, { boatTypeId: '$boatTypeId' })
35 wiredBoats(result) {
36 this.boats = result;
37 if (result.error) {
38 this.error = result.error;
39 this.boats = undefined;
40 }
41 this.isLoading = false;
42 this.notifyLoading(this.isLoading);
43 }
44
45 updateSelectedTile(event) {
46 this.selectedBoatId = event.detail.boatId;
47 this.sendMessageService(this.selectedBoatId);
48 }
49
50 handleSave(event) {
51 this.notifyLoading(true);
52 const recordInputs = event.detail.draftValues.slice().map(draft=>{
53 const fields = Object.assign({}, draft);
54 return {fields};
55 });
56
57 console.log(recordInputs);
58 const promises = recordInputs.map(recordInput => updateRecord(recordInput));
59 Promise.all(promises).then(res => {
60 this.dispatchEvent(
61 new ShowToastEvent({
62 title: SUCCESS_TITLE,
63 message: MESSAGE_SHIP_IT,
64 variant: SUCCESS_VARIANT
65 })
66 );
67 this.draftValues = [];
68 return this.refresh();
69 }).catch(error => {
70 this.error = error;
71 this.dispatchEvent(
72 new ShowToastEvent({
73 title: ERROR_TITLE,
74 message: CONST_ERROR,
75 variant: ERROR_VARIANT
76 })
77 );
78 this.notifyLoading(false);
79 }).finally(() => {
80 this.draftValues = [];
81 });
82 }
83 @api
84 async refresh() {
85 this.isLoading = true;
86 this.notifyLoading(this.isLoading);
87 await refreshApex(this.boats);
88 this.isLoading = false;
89 this.notifyLoading(this.isLoading);
90 }
91
92
93 notifyLoading(isLoading) {
94 if (isLoading) {
95 this.dispatchEvent(new CustomEvent('loading'));
96 } else {
97 this.dispatchEvent(CustomEvent('doneloading'));
98 }
99 }
100
101 sendMessageService(boatId) {
102 publish(this.messageContext, BoatMC, { recordId : boatId });
103 }
104 }

boatSearchResults.html:嵌套子组件,展示UI

<template>
<lightning-tabset variant="scoped">
<lightning-tab label="Gallery">
<template if:true={boats.data}>
<div class="slds-scrollable_y">
<lightning-layout horizontal-align="center" multiple-rows>
<template for:each={boats.data} for:item="boat">
<lightning-layout-item key={boat.Id} padding="around-small" size="12" small-device-size="6"
medium-device-size="4" large-device-size="3">
<c-boat-tile boat={boat} selected-boat-id={selectedBoatId}
onboatselect={updateSelectedTile}></c-boat-tile>
</lightning-layout-item>
</template>
</lightning-layout>
</div>
</template>
</lightning-tab>
<lightning-tab label="Boat Editor">
<!-- Scrollable div and lightning datatable go here -->
<template if:true={boats.data}>
<div class="slds-scrollable_y">
<lightning-datatable key-field="Id" data={boats.data} columns={columns} onsave={handleSave}
draft-values={draftValues} hide-checkbox-column show-row-number-column>
</lightning-datatable>
</div>
</template>
</lightning-tab>
<lightning-tab label="Boats Near Me">
<!-- boatsNearMe component goes here -->
<c-boats-near-me boat-type-id={boatTypeId}></c-boats-near-me>
</lightning-tab>
</lightning-tabset>
</template>

boatSearch.js:处理子组件事件 / navigation

import { LightningElement , track } from 'lwc';
import { NavigationMixin } from 'lightning/navigation' // imports
export default class BoatSearch extends NavigationMixin(LightningElement) {
@track isLoading = false; // Handles loading event
handleLoading(event) {
this.isLoading = true;
} // Handles done loading event
handleDoneLoading(event) {
this.isLoading = false;
} // Handles search boat event
// This custom event comes from the form
searchBoats(event) {
const boatTypeId = event.detail.boatTypeId;
this.template.querySelector("c-boat-search-results").searchBoats(boatTypeId);
} createNewBoat() {
this[NavigationMixin.Navigate]({
type: 'standard__objectPage',
attributes: {
objectApiName: 'Boat__c',
actionName: 'new'
}
});
}
}

boatSearch.html: UI展示

<template>
<lightning-layout multiple-rows>
<!-- Top -->
<lightning-layout-item size="12">
<lightning-card title="Find a Boat">
<!-- New Boat button goes here -->
<lightning-button label="New Boat" onclick={createNewBoat}></lightning-button>
<p class="slds-var-p-horizontal_small">
<!-- boatSearchForm component goes here -->
<c-boat-search-form onsearch={searchBoats}></c-boat-search-form>
</p>
</lightning-card>
</lightning-layout-item>
<!-- Bottom -->
<lightning-layout-item size="12" class="slds-p-top_small slds-is-relative">
<!-- Spinner goes here -->
<template if:true={isLoading}>
<lightning-spinner alternative-text="Loading" variant="brand"></lightning-spinner>
</template>
<!-- boatSearchResults goes here -->
<c-boat-search-results onloading={handleLoading} ondoneloading={handleDoneLoading}></c-boat-search-results>
<!-- onloading={handleLoading} ondoneloading={handleDoneLoading} -->
</lightning-layout-item>
</lightning-layout>
</template>

boatReviews.js:wire adapter / navigation / 父子嵌套

 1 import { LightningElement, api } from 'lwc';
2 import getAllReviews from '@salesforce/apex/BoatDataService.getAllReviews';
3 import { NavigationMixin } from 'lightning/navigation';
4 import { refreshApex } from '@salesforce/apex';
5
6 export default class BoatReviews extends NavigationMixin(LightningElement) {
7 // Private
8 boatId;
9 error;
10 boatReviews = [];
11 isLoading = false;
12
13 // Getter and Setter to allow for logic to run on recordId change
14 @api
15 get recordId() {
16 return this.boatId;
17 }
18
19 set recordId(value) {
20 //sets boatId attribute
21 this.setAttribute('boatId', value);
22 //sets boatId assignment
23 this.boatId = value;
24 //get reviews associated with boatId
25 this.getReviews();
26 }
27
28 // Getter to determine if there are reviews to display
29 get reviewsToShow() {
30 return this.boatReviews && this.boatReviews.length > 0 ? true : false;
31 }
32
33 // Public method to force a refresh of the reviews invoking getReviews
34 @api
35 refresh() {
36 refreshApex(this.getReviews());
37 }
38
39 // Imperative Apex call to get reviews for given boat
40 // returns immediately if boatId is empty or null
41 // sets isLoading to true during the process and false when it’s completed
42 // Gets all the boatReviews from the result, checking for errors.
43 getReviews() {
44 if(this.boatId == null || this.boatId == '') {
45 return;
46 }
47 this.isLoading = true;
48 this.error = undefined;
49 getAllReviews({boatId:this.recordId})
50 .then(result=>{
51 this.boatReviews = result;
52 this.isLoading = false;
53 }).catch(error=>{
54 this.error = error.body.message;
55 }).finally(() => {
56 this.isLoading = false;
57 });
58 }
59
60 // Helper method to use NavigationMixin to navigate to a given record on click
61 navigateToRecord(event) {
62 this[NavigationMixin.Navigate]({
63 type: "standard__recordPage",
64 attributes: {
65 recordId: event.target.dataset.recordId,
66 actionName: "view"
67 }
68 });
69 }
70 }

boatReviews.html

<template>
<!-- div for when there are no reviews available -->
<template if:false={reviewsToShow}>
<div class="slds-feed, reviews-style, slds-is-relative, slds-scrollable_y "><div class="slds-align_absolute-center">No reviews available</div></div>
</template> <!-- div for when there are reviews available -->
<div>
<!-- insert spinner -->
<template if:true={isLoading}>
<lightning-spinner variant="brand" alternative-text="Loading" size="small"></lightning-spinner>
</template>
<template if:true={reviewsToShow}>
<ul class="slds-feed__list">
<!-- start iteration -->
<template for:each={boatReviews} for:item="boatReview">
<li class="slds-feed__item" key={boatReview.Id}>
<article class="slds-post">
<header class="slds-post__header slds-media">
<div class="slds-media__figure">
<!-- display the creator’s picture -->
<lightning-avatar
variant="circle"
src={boatReview.CreatedBy.SmallPhotoUrl}
initials="AW"
fallback-icon-name="standard:user"
alternative-text={boatReview.CreatedBy.Name}
class="slds-m-right_small">
</lightning-avatar>
</div>
<div class="slds-media__body">
<div class="slds-grid slds-grid_align-spread slds-has-flexi-truncate">
<p>
<!-- display creator’s name -->
<a data-record-id={boatReview.CreatedBy.Id} title={boatReview.CreatedBy.Name} onclick={navigateToRecord}>{boatReview.CreatedBy.Name}
</a>
<span><!-- display creator’s company name -->
{boatReview.CreatedBy.CompanyName}
</span>
</p>
</div>
<p class="slds-text-body_small">
<!-- display created date -->
<lightning-formatted-date-time value={boatReview.CreatedDate}></lightning-formatted-date-time>
</p>
</div>
</header>
<div class="slds-text-longform">
<p class="slds-text-title_caps"><!-- display Name -->{boatReview.Name}</p>
<!-- display Comment__c -->
<lightning-formatted-rich-text value={boatReview.Comment__c}></lightning-formatted-rich-text>
</div>
<!-- display five star rating on readonly mode -->
<c-five-star-rating read-only value={boatReview.Rating__c}></c-five-star-rating>
</article>
</li> </template>
<!-- end iteration -->
</ul>
</template>
</div>
</template>

boatReviews.css

.reviews-style {
max-height: 250px;
}

boatAddReviewForm.js:lightning data service / wire adapter / toast

 1 import { LightningElement, api, track } from 'lwc';
2 import { createRecord } from 'lightning/uiRecordApi';
3 import { ShowToastEvent } from 'lightning/platformShowToastEvent';
4 import NAME_FIELD from '@salesforce/schema/BoatReview__c.Name';
5 import COMMENT_FIELD from '@salesforce/schema/BoatReview__c.Comment__c';
6 import RATING_FIELD from '@salesforce/schema/BoatReview__c.Rating__c';
7 import BOAT_REVIEW_OBJECT from '@salesforce/schema/BoatReview__c';
8 import BOAT_FIELD from '@salesforce/schema/BoatReview__c.Boat__c';
9
10 const SUCCESS_TITLE = 'Review Created!';
11 const SUCCESS_VARIANT = 'success';
12
13 export default class BoatAddReviewForm extends LightningElement {
14 @api boat;
15 boatId;
16 nameField = NAME_FIELD;
17 commentField = COMMENT_FIELD;
18 boatReviewObject = BOAT_REVIEW_OBJECT;
19 rating = 0;
20 review = '';
21 title = '';
22 comment = '';
23
24
25 @api
26 get recordId() {
27 return this.boatId;
28 }
29
30 set recordId(value) {
31 this.boatId = value;
32 }
33
34 // Gets user rating input from stars component
35 handleRatingChanged(event) {
36 this.rating = event.detail.rating;
37 }
38
39 // Custom submission handler to properly set Rating
40 // This function must prevent the anchor element from navigating to a URL.
41 // form to be submitted: lightning-record-edit-form
42 handleSubmit(event) {
43 event.preventDefault();
44 const fields = event.detail.fields;
45 fields.Boat__c = this.boatId;
46 fields.Rating__c = this.rating;
47 this.template.querySelector('lightning-record-edit-form').submit(fields);
48 }
49
50 // Shows a toast message once form is submitted successfully
51 // Dispatches event when a review is created
52 handleSuccess() {
53 // TODO: dispatch the custom event and show the success message
54 const evt = new ShowToastEvent({
55 title: SUCCESS_TITLE,
56 variant: SUCCESS_VARIANT
57 });
58 this.dispatchEvent(evt);
59 this.dispatchEvent(new CustomEvent('createreview'));
60 this.handleReset();
61 }
62
63 // Clears form data upon submission
64 // TODO: it must reset each lightning-input-field
65 handleReset() {
66 const inputFields = this.template.querySelectorAll(
67 'lightning-input-field'
68 );
69 if (inputFields) {
70 inputFields.forEach(field => {
71 field.reset();
72 });
73 }
74 }
75 }

boatAddReviewForm.html

<template>
<lightning-record-edit-form object-api-name="BoatReview__c" onsuccess={handleSuccess} onsubmit={handleSubmit}>
<lightning-layout vertical-align="stretch" multiple-rows="true">
<lightning-messages>
</lightning-messages>
<lightning-layout-item size="12">
<lightning-input-field field-name="Name">
</lightning-input-field>
</lightning-layout-item>
<lightning-layout-item size="12">
<p>Rating:</p>
<c-five-star-rating rating-value={rating} onratingchange={handleRatingChanged}>
</c-five-star-rating>
</lightning-layout-item>
<lightning-layout-item>
<lightning-input-field field-name="Comment__c">
</lightning-input-field>
</lightning-layout-item>
<div class="slds-align--absolute-center">
<lightning-button label="Submit" type="submit" icon-name="utility:save"></lightning-button>
</div>
</lightning-layout>
</lightning-record-edit-form>
</template>

boatDetailTabs: 订阅 lightning message service / wire adapter / navigation

// Custom Labels Imports
// import labelDetails for Details
// import labelReviews for Reviews
// import labelAddReview for Add_Review
// import labelFullDetails for Full_Details
// import labelPleaseSelectABoat for Please_select_a_boat
// Boat__c Schema Imports
// import BOAT_ID_FIELD for the Boat Id
// import BOAT_NAME_FIELD for the boat Name
import { LightningElement, api,wire } from 'lwc';
import labelDetails from '@salesforce/label/c.Details';
import labelReviews from '@salesforce/label/c.Reviews';
import labelAddReview from '@salesforce/label/c.Add_Review';
import labelFullDetails from '@salesforce/label/c.Full_Details';
import labelPleaseSelectABoat from '@salesforce/label/c.Please_select_a_boat';
import BOAT_ID_FIELD from '@salesforce/schema/Boat__c.Id';
import BOAT_NAME_FIELD from '@salesforce/schema/Boat__c.Name';
import { getRecord,getFieldValue } from 'lightning/uiRecordApi';
import BOATMC from '@salesforce/messageChannel/BoatMessageChannel__c';
import { APPLICATION_SCOPE,MessageContext, subscribe } from 'lightning/messageService';
import BoatReviews from 'c/boatReviews';
const BOAT_FIELDS = [BOAT_ID_FIELD, BOAT_NAME_FIELD];
import {NavigationMixin} from 'lightning/navigation';
export default class BoatDetailTabs extends NavigationMixin(LightningElement) {
@api boatId; label = {
labelDetails,
labelReviews,
labelAddReview,
labelFullDetails,
labelPleaseSelectABoat,
}; // Decide when to show or hide the icon
// returns 'utility:anchor' or null
get detailsTabIconName() {
return this.wiredRecord && this.wiredRecord.data ? 'utility:anchor' : null;
} // Utilize getFieldValue to extract the boat name from the record wire
@wire(getRecord,{recordId: '$boatId', fields: BOAT_FIELDS})
wiredRecord; get boatName() {
return getFieldValue(this.wiredRecord.data, BOAT_NAME_FIELD);
} // Private
subscription = null;
// Initialize messageContext for Message Service
@wire(MessageContext)
messageContext; // Subscribe to the message channel
subscribeMC() {
if(this.subscription) { return; }
// local boatId must receive the recordId from the message
this.subscription = subscribe(
this.messageContext,
BOATMC,
(message) => {
this.boatId = message.recordId;
},
{ scope: APPLICATION_SCOPE }
);
} // Calls subscribeMC()
connectedCallback() {
this.subscribeMC();
} // Navigates to record page
navigateToRecordViewPage() {
this[NavigationMixin.Navigate]({
type: "standard__recordPage",
attributes: {
recordId: this.boatId,
actionName: "view"
}
});
} // Navigates back to the review list, and refreshes reviews component
handleReviewCreated() {
this.template.querySelector('lightning-tabset').activeTabValue = 'reviews';
this.template.querySelector('c-boat-reviews').refresh();
// BoatReviews.refresh();
}
}

boatDetailTabs.html

<template>
<template if:false={wiredRecord.data}>
<!-- lightning card for the label when wiredRecord has no data goes here -->
<lightning-card class= "slds-align_absolute-center no-boat-height">
<span>{label.labelPleaseSelectABoat}</span>
</lightning-card>
</template>
<template if:true={wiredRecord.data}>
<!-- lightning card for the content when wiredRecord has data goes here -->
<lightning-card>
<lightning-tabset variant="scoped">
<lightning-tab label={label.labelDetails}>
<lightning-card icon-name={detailsTabIconName} title={boatName}>
<lightning-button slot="actions" title={boatName} label={label.labelFullDetails} onclick={navigateToRecordViewPage}></lightning-button>
<lightning-record-view-form density="compact"
record-id={boatId}
object-api-name="Boat__c">
<lightning-output-field field-name="BoatType__c" class="slds-form-element_1-col"></lightning-output-field>
<lightning-output-field field-name="Length__c" class="slds-form-element_1-col"></lightning-output-field>
<lightning-output-field field-name="Price__c" class="slds-form-element_1-col"></lightning-output-field>
<lightning-output-field field-name="Description__c" class="slds-form-element_1-col"></lightning-output-field>
</lightning-record-view-form>
</lightning-card>
</lightning-tab>
<lightning-tab label={label.labelReviews} value="reviews"><c-boat-reviews record-id={boatId}></c-boat-reviews></lightning-tab>
<lightning-tab label={label.labelAddReview}> <c-boat-add-review-form record-id={boatId} oncreatereview={handleReviewCreated}></c-boat-add-review-form></lightning-tab>
</lightning-tabset>
</lightning-card>
</template>
</template>

boatDetailTabs.css

.no-boat-height {
height: 3rem;
}

boatMap.js: lightning message service / wire adapter

// import BOATMC from the message channel
import { LightningElement,wire,api,track } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import { APPLICATION_SCOPE,subscribe,MessageContext,unsubscribe } from 'lightning/messageService';
import BOATMC from '@salesforce/messageChannel/BoatMessageChannel__c';
// Declare the const LONGITUDE_FIELD for the boat's Longitude__s
// Declare the const LATITUDE_FIELD for the boat's Latitude
// Declare the const BOAT_FIELDS as a list of [LONGITUDE_FIELD, LATITUDE_FIELD];
const LONGITUDE_FIELD = 'Boat__c.Geolocation__Longitude__s';
const LATITUDE_FIELD = 'Boat__c.Geolocation__Latitude__s';
const BOAT_FIELDS = [LONGITUDE_FIELD, LATITUDE_FIELD];
export default class BoatMap extends LightningElement {
// private
subscription = null;
boatId;
// Getter and Setter to allow for logic to run on recordId change
// this getter must be public
@api get recordId() {
return this.boatId;
}
set recordId(value) {
this.setAttribute('boatId', value);
this.boatId = value;
}
//public
@track error = undefined;
@track mapMarkers = [];
// Initialize messageContext for Message Service
@wire(MessageContext)
messageContext;
// Getting record's location to construct map markers using recordId
// Wire the getRecord method using ('$boatId')
@wire(getRecord,{recordId:'$boatId',fields:BOAT_FIELDS})
wiredRecord({ error, data }) {
// Error handling
if (data) {
this.error = undefined;
const longitude = data.fields.Geolocation__Longitude__s.value;
const latitude = data.fields.Geolocation__Latitude__s.value;
console.log('*** longitude : ' + longitude);
console.log('*** latitude : ' + latitude);
this.updateMap(longitude, latitude);
} else if (error) {
this.error = error;
this.boatId = undefined;
this.mapMarkers = [];
}
}
// Encapsulate logic for Lightning message service subscribe and unsubsubscribe
subscribeMC(){
if(!this.subscription){
this.subscription = subscribe(
this.messageContext,
BOATMC,
(message) => {this.boatId = message.recordId},
{ scope: APPLICATION_SCOPE }
);
}
} unsubscribeToMessageChannel() {
unsubscribe(this.subscription);
this.subscription = null;
}
// Runs when component is connected, subscribes to BoatMC
connectedCallback() {
// recordId is populated on Record Pages, and this component
// should not update when this component is on a record page.
if (this.subscription || this.recordId) {
return;
}
this.subscribeMC();
// Subscribe to the message channel to retrieve the recordID and assign it to boatId.
}
disconnectedCallback() {
this.unsubscribeToMessageChannel();
}
// Creates the map markers array with the current boat's location for the map.
updateMap(Longitude, Latitude) {
this.mapMarkers = [{
location : {
Latitude : Latitude,
Longitude : Longitude
}
}];
}
// Getter method for displaying the map component, or a helper method.
get showMap() {
return this.mapMarkers.length > 0;
}
}

boatMap.html

<template>
<lightning-card title="Current Boat Location" icon-name="action:map">
<template if:true={showMap}>
<lightning-map map-markers={mapMarkers} zoom-level="10"></lightning-map>
</template>
<template if:false={showMap}>
<span class="slds-align_absolute-center">
Please select a boat to see its location!
</span>
</template>
</lightning-card>
</template>

一览图展示各组件位置及关系,我们需要创建一个 single app,将 boatSearch / boatDetailTabs 以及 boatMap拖动到指定位置即可。

通过以上代码即可实现一个lwc的简单的app。

总结:篇中根据lwc superbadge进行了代码的整理,代码并非最优版,感兴趣小伙伴自行优化,篇中有错误欢迎指出,有不懂欢迎留言。

Salesforce LWC学习(三十) lwc superbadge项目实现的相关教程结束。

《Salesforce LWC学习(三十) lwc superbadge项目实现.doc》

下载本文的Word格式文档,以方便收藏与打印。