Exercise 5uiRecordApi, Design Parameters and Component Communication

Objectives

  • Use the uiRecordApi to make the component reactive
  • Leverage events for cross-component communication
  • Create Design parameters for Lightning App Builder
  • Restrict the use of the component to a specific object
  • Give users feedback for loading data and no data states

Step 1 - Leverage uiRecordApi

  1. On the Property Detail page, edit the price of the property (raise it by $200000).
  2. Click Save, and notice that the component doesn’t change.
  3. Refresh the page, and notice that the Similar Properties component is showing different properties.
  4. In VS Code, open the similarProperties.js if it isn’t already open.
  5. Add the following import:

     import { getRecord } from 'lightning/uiRecordApi';
    
  6. Declare a constant named fields as an array to define which fields to retrieve:

     const fields = [
         'Property__c.Name',
         'Property__c.Price__c',
         'Property__c.Status__c',
         'Property__c.Beds__c',
         'Property__c.Broker__c'
     ]
    
  7. Add these tracked variables:

     @track property;
     @track price;
     @track beds;
    
  8. Add a new @wire to retrieve the desired fields for the current record:

     @wire(getRecord, {recordId: '$recordId', fields})
     wiredProperty(value) {
         if(value.data) {
             this.property = value.data;
             this.price = this.property.fields.Price__c.value;
             this.beds = this.property.fields.Beds__c.value;
         } else if (value.error) {
             console.log("OOOPS: ", value.error)
         }
     }
    
  9. Add price: '$price' and beds: '$beds' to the parameters being passed to the findProperties Apex method.
  10. Save the file, and push to your scratch org.
  11. Refresh the Property Detail page.
  12. Edit the price of the property, and notice that the component performs a new search.

    Even though the Apex method findProperties only takes recordId and priceRange as parameters, the fact that $price is reactive causes the method to be called when its value changes.

  13. Make an edit to a record in the Similar Properties list, increasing the price by more than $50,000. Notice that the component doesn’t refresh.

Step 2 - Using Lightning Message Service

  1. In VS Code file pane, right-click on the force-app/main/default folder and choose New Folder.
  2. Name the folder, messageChannels.
  3. Right-click on the new folder, and choose New File.
  4. Enter Properties.messageChannel-meta.xml as the file name.
  5. Paste the following into the new file:

     <?xml version="1.0" encoding="UTF-8"?>
     <LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
         <masterLabel>Example Channel</masterLabel>
         <isExposed>true</isExposed>
         <description>This is an example Lightning Messages Channel.</description>
         <lightningMessageFields>
             <description>Where did this message originate</description>
             <fieldName>source</fieldName>
         </lightningMessageFields>
         <lightningMessageFields>
             <description>What is this message about</description>
             <fieldName>title</fieldName>
         </lightningMessageFields>
         <lightningMessageFields>
             <description>What is the message</description>
             <fieldName>message</fieldName>
         </lightningMessageFields>
     </LightningMessageChannel>
    
  6. Save the file.
  7. Switch to SimilarProperty.js.
  8. Add the following to the imports for the component:

     import { publish, createMessageContext, releaseMessageContext } from 'lightning/messageService';
     import MESSAGE_CHANNEL from "@salesforce/messageChannel/Properties__c";
    
  9. Add context = createMessageContext(); to the class on a new line after @track decorators.
  10. Add publish(this.context, MESSAGE_CHANNEL, this); to the end of the handleSuccess method.
  11. Save the file, then switch to similarProperties.js.
  12. Add the following import:

     import { subscribe, createMessageContext, releaseMessageContext } from 'lightning/messageService';
     import MESSAGE_CHANNEL from "@salesforce/messageChannel/Properties__c";
     import { refreshApex } from '@salesforce/apex';
    
  13. Add the following after the @track declarations:

     context = createMessageContext();
     subscription = null;
    
  14. Add the following methods to the end of the class:

    connectedCallback() {
        if (this.subscription) {
            return;
        }
        this.subscription = subscribe(this.context, MESSAGE_CHANNEL, (message) => {
            this.refreshSelection(message);
        }); 
    }
    
    disconnectedCallback() {
        releaseMessageContext(this.context);
    }
        
    refreshSelection() {
        refreshApex(this.wiredRecords);
    }
    
  15. Save the file, push to your scratch org, then refresh the Property page.
  16. Make an edit to a record in the Similar Properties list, changing the price +/- $100000.
  17. Notice that the component refreshes.

Step 3 - Using Design Parameters

  1. Add these new properties to the SimilarProperties class.

     @api searchCriteria = 'Price';
     @api priceRange = '100000';
     @track cardTitle;
    
  2. Add the searchCriteria property to the parameters being sent to the Apex class and change the value of priceRange to be dynamic like this:

     priceRange: '$priceRange',
     price: '$price',
     beds: '$beds',
     searchCriteria: '$searchCriteria'
    
  3. In order to make the card title dynamic, change its title to title={cardTitle} in similarProperties.html, then save the file.
  4. Add a renderedCallback to the SimilarProperties class to set the card title:

     renderedCallback() {
         this.cardTitle = 'Similar Properties by ' + this.searchCriteria;
     }
    
  5. Save the file.
  6. Add the following configuration to the LightningComponentBundle in similarProperties.js-meta.xml:

     <targetConfigs>
         <targetConfig targets="lightning__RecordPage">
             <property name="searchCriteria" datasource="Price,Bedrooms" label="Search Criteria" type="String" default="Price" />
             <property name="priceRange" type="String" label="Price Range" default="100000" />
             <objects>
                 <object>Property__c</object>
             </objects>
         </targetConfig>
     </targetConfigs>
    
  7. Save the file, push to your scratch org, then reload the Property Detail page.
  8. Click the Setup icon and choose Edit Page.
  9. Select the Similar Properties component on the page.
  10. Change the Price Range in the right-hand column of App Builder.
  11. Hit Return or Tab, and notice the component reloads on the page with the new results.
  12. Change the Search Criteria to Bedrooms, and notice the component doesn’t fetch properties with the same number of bedrooms.

Step 4 - Update the Apex Class

  1. Open SimilarPropertyController.cls if it is not already open.
  2. Replace the contents of the SimilarPropertyController class with:

     @AuraEnabled(cacheable=true)
     public static List<Property__c> getSimilarProperties (Id recordId, String searchCriteria, Decimal beds, Decimal price, Decimal priceRange ) {
              if (searchCriteria == 'Bedrooms') {
                  return [
                      SELECT Id, Name, Beds__c, Baths__c, Price__c, Broker__c, Status__c, Thumbnail__c
                      FROM Property__c WHERE Id != :recordId AND Beds__c = :beds
                  ];
              } else {
                  Decimal range;
                  if (priceRange == null) {
                      range = 100000;
                  } else {
                      range = priceRange;
                  }
                  return [
                      SELECT Id, Name, Beds__c, Baths__c, Price__c, Broker__c, Status__c, Thumbnail__c
                      FROM Property__c WHERE Id != :recordId AND Price__c > :price - range AND Price__c < :price + range
                  ];
              }
     }
    
  3. Save the file.
  4. In similarProperties.js, update the import for findProperties to import getSimilarProperties from SimilarPropertyController:

     import findProperties from '@salesforce/apex/SimilarPropertyController.getSimilarProperties';
    
  5. Save the file, push to your scratch org.
  6. In App Builder, click the Refresh button to reload the page.
  7. Select the Similar Properties component on the page, then change the Search Criteria to Bedrooms. Notice that the component now displays properties with the same number of bedrooms as the current property.
  8. Click the Reload icon in the left-hand column of App Builder to load the newest copy of your components.
  9. Drag another instance of the Similar Properties component into the top of right-hand column.
  10. Change the Price to 50000, then hit Tab or Return. Notice the component updates with new results.

Step 5 - Add a Custom Icon for Lightning App Builder

  1. In VS Code, right-click on the similarProperties component folder in the File Explorer.
  2. Choose New File.
  3. Name the file similarProperties.svg.
  4. In a new browser tab, navigate back to the SLDS site if it is not still open.
  5. Navigate to the Icons section of the site.
  6. Scroll down to locate custom85 in the Custom section of icons.
  7. (Informational Only) Click on the Downloads link in the navigation panel, and then scroll down to the Icons section and click the Download button.
  8. (Informational Only) After navigating to the downloaded zip file and unzipping it, open the folder and then open the custom folder.
  9. (Informational Only) Locate the custom85.svg file and open it in a text editor.
  10. (Informational Only) Copy the <path> tag from the SVG.
  11. (Informational Only) In the Developer Console, switch to SimilarProperties.svg.
  12. (Informational Only) Replace the second <path> tag with the one you just copied.
  13. (Informational Only) At the beginning of the <path> you just pasted, add fill=”#fff” before the “d” attribute.
  14. (Informational Only) Change width="120px" height="120px" viewBox="0 0 120 120" in the <svg> tag to:

    width="100px" height="100px" viewBox="0 0 100 100"
    
  15. (Informational Only) Change the fill of the first <path> to #F26891.
  16. Paste the following into similarProperties.svg:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
            <path d="M120,108 C120,114.6 114.6,120 108,120 L12,120 C5.4,120 0,114.6 0,108 L0,12 C0,5.4 5.4,0 12,0 L108,0 C114.6,0 120,5.4 120,12 L120,108 L120,108 Z" id="Shape" fill="#2A739E"/>
            <path fill="#FFF" d="m78 24h-50v-2c0-1.1-0.9-2-2-2h-4c-1.1 0-2 0.9-2 2v56c0 1.1 0.9 2 2 2h4c1.1 0 2-0.9 2-2v-46h50c1.1 0 2-0.9 2-2v-4c0-1.1-0.9-2-2-2z m-4 14h-34c-3.3 0-6 2.7-6 6v22c0 3.3 2.7 6 6 6h34c3.3 0 6-2.7 6-6v-22c0-3.3-2.7-6-6-6z m-5.5 17h-2.5v10c0 0.6-0.4 1-1 1h-4c-0.6 0-1-0.4-1-1v-6c0-0.6-0.4-1-1-1h-4c-0.6 0-1 0.4-1 1v6c0 0.6-0.4 1-1 1h-4c-0.6 0-1-0.4-1-1v-10h-2.5c-0.5 0-0.7-0.6-0.3-0.9l11.2-10.9c0.4-0.3 0.9-0.3 1.3 0l11.2 10.9c0.3 0.3 0.1 0.9-0.4 0.9z"></path>
        </g>
    </svg>
    
  17. Save the file, and push to your scratch org.
  18. Switch back to Lightning App Builder, and click the Refresh button at the top of the components’ list.

    Screenshot: Similar Properties component with custom icon

  19. Click the Save button in App Builder, then click the Back button to return to the Property Detail page.

Step 6 - Show/Hide a Spinner

  1. Switch to similarProperties.html.
  2. Add the following at the bottom of the markup before the closing </lightning-card>:

     <template if:false={props}>
         <lightning-spinner variant="brand"
                            size="large"></lightning-spinner>
     </template>
    
  3. Save the file.

  4. Right-click on the similarProperties folder in the File Explorer in VS Code, and select New File.
  5. Name the file similarProperties.css.
  6. Add the following to the CSS file:

     .spacer {
         min-height: 10rem;
     }
    
  7. Save the file.
  8. Add the new class to the <div> inside the <lightning-card>.
  9. Save the file, then push it to your scratch org.
  10. Reload the Property Record page. Notice the spinner briefly appears, then disappears once the data has loaded.
  11. Change the price of the current property to 200000 and notice that the Similar Properties by Price component is simply blank, with no feedback for the user.

Step 7 - Conditionally Render Content

  1. Switch back to similarProperties.html.
  2. Wrap the <ul> ... </ul> in a template if:

     <template if:true={showRecords}>
         <!-- the ul is here -->
     </template>
    
  3. Add the following on a new line after the opening <div> tag:

     <template if:false={showRecords}>
         <h3 class="slds-text-heading_small slds-text-color_error">No similar properties found.</h3>
     </template>
    
  4. Switch to similarProperties.js and add the following after the renderedCallback method:

     get showRecords() {
         if (this.props.data) {
             if (this.props.data.length === 0) {
                 return false;
             } else {
                 return true;
             }
         } else if (this.props.error) {
             return 'Houston, we have a problem: ' + this.props.error;
         }
     }
    
  5. Save the file, then push to your scratch org.
  6. Refresh the page. You should now see the component reporting now similar properties by price.
  7. Change the price of the property back to 850000 to confirm the component is still rendering similar properties properly.

Need a last second rescue? Check your code against the following:

SimilarProperties

similarProperties.html
<template>
    <lightning-card title={cardTitle}
                    icon-name="custom:custom85">
        <div class="slds-m-around_medium spacer">
            <template if:false={showRecords}>
                <h3 class="slds-text-heading_small slds-text-color_error">No similar properties found.</h3>
            </template>
            <template if:true={showRecords}>
                <ul class="slds-list_vertical slds-has-dividers_top-space">
                    <template for:each={props.data}
                              for:item="item"
                              if:true={props.data}>
                        <li key={item.Id}
                            class="slds-list__item">
                            <c-similar-Property item={item}></c-similar-Property>
                        </li>
                    </template>
                    <template if:true={props.error}>
                        <li class="slds-list__item">
                            <h3 class="slds-text-heading_small slds-text-color_error">{props.error}</h3>
                        </li>
                    </template>
                </ul>
            </template>
        </div>
        <template if:false={props.data}>
            <lightning-spinner variant="brand" size="large"></lightning-spinner>
        </template>
    </lightning-card>
</template>
similarProperties.js
import { LightningElement, api, wire, track } from 'lwc';
import findProperties from '@salesforce/apex/SimilarPropertyController.getSimilarProperties';
import { getRecord } from 'lightning/uiRecordApi';
import { subscribe, createMessageContext, releaseMessageContext } from 'lightning/messageService';
import MESSAGE_CHANNEL from "@salesforce/messageChannel/Properties__c";
import { refreshApex } from '@salesforce/apex';

const fields = [
    'Property__c.Name',
    'Property__c.Price__c',
    'Property__c.Status__c',
    'Property__c.Beds__c',
    'Property__c.Broker__c'
]

export default class SimilarProperties extends LightningElement {
    @api recordId;
    @track props;
    @track errorMsg;
    @track property;
    @track price;
    @track beds;
    @api searchCriteria = 'Price';
    @api priceRange = '100000';
    @track cardTitle;

    context = createMessageContext();
    subscription = null;

    @wire(findProperties, {
        recordId: '$recordId',
        priceRange: '100000',
        price: '$price',
        beds: '$beds',
        searchCriteria: '$searchCriteria'
    })
    props

    @wire(getRecord, { recordId: '$recordId', fields })
    wiredProperty(value) {
        if (value.data) {
            this.property = value.data;
            this.price = this.property.fields.Price__c.value;
            this.beds = this.property.fields.Beds__c.value;
        } else if (value.error) {
            console.log("OOOPS: ", value.error)
        }
    }

    connectedCallback() {
        if (this.subscription) {
            return;
        }
        this.subscription = subscribe(this.context, MESSAGE_CHANNEL, (message) => {
            this.refreshSelection(message);
        });
    }

    disconnectedCallback() {
        releaseMessageContext(this.context);
    }

    refreshSelection() {
        refreshApex(this.props);
    }

    renderedCallback() {
        this.cardTitle = 'Similar Properties by ' + this.searchCriteria;
    }

    get showRecords() {
        if (this.props.data) {
            if (this.props.data.length === 0) {
                return false;
            } else {
                return true;
            }
        } else if (this.props.error) {
            return 'Houston, we have a problem: ' + this.props.error;
        }
    }
}
SimilarProperties.css
.spacer {
    min-height: 10rem;
}
similarProperties.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="similarProps">
    <apiVersion>45.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Similar Properties</masterLabel>
    <description>This component searches for similar properties.</description>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <property name="searchCriteria" datasource="Price,Bedrooms" label="Search Criteria" type="String" default="Price" />
            <property name="priceRange" type="String" label="Price Range" default="50000" />
            <objects>
                <object>Property__c</object>
            </objects>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

SimilarProperty

similarProperty.html
<template>
    <div class="slds-media">
        <div class="slds-media__figure">
            <img src={item.Thumbnail__c} class="slds-avatar_large slds-avatar_circle" alt={item.Title_c} />
            </div>
            <div class="slds-media__body">
                <div class="slds-grid slds-hint-parent">
                    <a onclick={navigateToRecord}>
                        <h3 class="slds-text-heading_small slds-m-bottom_xx-small">{item.Name}</h3>
                    </a>
                    <template if:false={editMode}>
                        <lightning-button-icon icon-name="utility:edit"
                                               class="slds-col_bump-left"
                                               icon-class="slds-button__icon_hint"
                                               variant="bare"
                                               alternative-text="Edit Record"
                                               onclick={editRecord}></lightning-button-icon>
                    </template>
                </div>
                <template if:false={editMode}>
                    <lightning-record-view-form object-api-name="Property__c"
                                                record-id={item.Id}>
                        <lightning-layout multiple-rows>
                            <lightning-layout-item size="6">
                                <lightning-output-field field-name="Price__c"></lightning-output-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="6">
                                <lightning-output-field field-name="Beds__c"></lightning-output-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="6">
                                <lightning-output-field field-name="Baths__c"></lightning-output-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="6">
                                <lightning-output-field field-name="Status__c"></lightning-output-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="12">
                                <lightning-output-field field-name="Broker__c"></lightning-output-field>
                            </lightning-layout-item>
                        </lightning-layout>
                    </lightning-record-view-form>
                </template>
                <template if:true={editMode}>
                    <lightning-record-edit-form object-api-name="Property__c"
                                                record-id={item.Id}
                                                layout-type="Full"
                                                onsuccess={handleSuccess}
                                                onerror={handleError}>
                        <lightning-layout multiple-rows>
                            <lightning-layout-item size="6">
                                <lightning-input-field field-name="Price__c"></lightning-input-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="6">
                                <lightning-input-field field-name="Beds__c"></lightning-input-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="6">
                                <lightning-input-field field-name="Baths__c"></lightning-input-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="6">
                                <lightning-input-field field-name="Status__c"></lightning-input-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="12">
                                <lightning-input-field field-name="Broker__c"></lightning-input-field>
                            </lightning-layout-item>
                            <lightning-layout-item size="12">
                                <div class="slds-m-top_large slds-grid slds-grid_align-center">
                                    <lightning-button variant="neutral"
                                                      label="Cancel"
                                                      title="Cancel"
                                                      type="text"
                                                      onclick={handleCancel}
                                                      class="slds-m-right_small"></lightning-button>
                                    <lightning-button variant="brand"
                                                      label="Submit"
                                                      title="Submit"
                                                      type="submit"></lightning-button>
                                </div>
                            </lightning-layout-item>
                        </lightning-layout>
                    </lightning-record-edit-form>
                </template>
            </div>
        </div>
</template>
similarProperty.js
import { LightningElement, api, wire, track } from 'lwc';
import { NavigationMixin, CurrentPageReference } from 'lightning/navigation';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { publish, createMessageContext, releaseMessageContext } from 'lightning/messageService';
import MESSAGE_CHANNEL from "@salesforce/messageChannel/Properties__c";

export default class SimilarProperty extends NavigationMixin(LightningElement) {
    @api item;
    @track editMode = false;

    context = createMessageContext();

    // @wire(CurrentPageReference) pageRef;

    navigateToRecord() {
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: this.item.Id,
                objectApiName: 'Property__c',
                actionName: 'view',
            },
        });
    }

    editRecord() {
        this.editMode = true;
    }

    handleSuccess() {
        const evt = new ShowToastEvent({
            title: "Success!",
            message: "The record has been successfully saved.",
            variant: "success",
        });
        this.dispatchEvent(evt);
        this.editMode = false;
        publish(this.context, MESSAGE_CHANNEL, this);
    }

    handleError() {
        const evt = new ShowToastEvent({
            title: "Error!",
            message: "An error occurred while attempting to save the record.",
            variant: "error",
        });
        this.dispatchEvent(evt);
        this.editMode = false;
    }

    handleCancel(event) {
        this.editMode = false;
        event.preventDefault();
    }
}
SimilarPropertyController.cls
public with sharing class SimilarPropertyController {
    @AuraEnabled(cacheable=true)
    public static List<Property__c> getSimilarProperties (Id recordId, String searchCriteria, Decimal beds, Decimal price, Decimal priceRange ) {
        if (searchCriteria == 'Bedrooms') {
            return [
                SELECT Id, Name, Beds__c, Baths__c, Price__c, Broker__c, Status__c, Thumbnail__c
                FROM Property__c WHERE Id != :recordId AND Beds__c = :beds
            ];
        } else {
            Decimal range;
            if (priceRange == null) {
                range = 100000;
            } else {
                range = priceRange;
            }
            return [
                SELECT Id, Name, Beds__c, Baths__c, Price__c, Broker__c, Status__c, Thumbnail__c
                FROM Property__c WHERE Id != :recordId AND Price__c > :price - range AND Price__c < :price + range
            ];
        }
    }
}