Salesforce LWC學習(三十) lwc superbadge專案實現
本篇參考:https://trailhead.salesforce.com/content/learn/superbadges/superbadge_lwc_specialist
我們做lwc的學習時,因為很多人可能還沒接觸過lwc的專案,所以通過學習知道很多的知識點,但是可能沒有機會做到一個小專案,salesforce lwc superbadge正好可以在將知識點串起來基礎上,深化學習,當一個小專案練手。首先先按照上方的superbadge要求,安裝一個unlocked package,然後匯入到基礎資料。匯入以後資料以及表和基本的 component的殼子就都有了。一步一步進行分析。觀看以下視訊檢視效果:
(注: 如果沒有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進行了程式碼的整理,程式碼並非最優版,感興趣小夥伴自行優化,篇中有錯誤歡迎指出,有不懂歡迎留言。