diff --git a/README.md b/README.md index 2ca0497..acdfc3b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Salesforce Lookup Component (Lightning Web Component version) + Aura version is available [here](https://github.com/pozil/sfdc-ui-lookup).

@@ -8,23 +9,26 @@ Aura version is available [here](https://github.com/pozil/sfdc-ui-lookup). Lookup with dropdown open ## About + This is a generic & customizable lookup component built using Salesforce [Lightning Web Components](https://developer.salesforce.com/docs/component-library/documentation/lwc) and [SLDS](https://www.lightningdesignsystem.com/) style.
It does not rely on third party libraries and you have full control over its datasource. Features The lookup component provides the following features: -- customizable data source that can return mixed sObject types -- single or multiple selection mode -- client-side caching & request throttling -- built-in server request rate limit mechanism -- project is unit tested + +- customizable data source that can return mixed sObject types +- single or multiple selection mode +- client-side caching & request throttling +- built-in server request rate limit mechanism +- project is unit tested

Multiple or single entry lookup

## Documentation + Follow these steps in order to use the lookup component: ### 1) Write the search endpoint @@ -49,12 +53,14 @@ import apexSearch from '@salesforce/apex/SampleLookupController.search'; The lookup component exposes a `search` event that is fired when a search needs to be performed on the server-side. The parent component that contains the lookup must handle the `search` event: + ```xml ``` The `search` event handler calls the Apex `search` method and passes the results back to the lookup using the `setSearchResults` function: + ```js handleSearch(event) { const target = event.target; @@ -68,11 +74,11 @@ handleSearch(event) { } ``` - ### 4) Optionally handle selection changes The lookup component exposes a `selectionchange` event that is fired when the selection of the lookup changes. The parent component that contains the lookup can handle the `selectionchange` event: + ```xml @@ -80,6 +86,7 @@ The parent component that contains the lookup can handle the `selectionchange` e ``` The `selectionchange` event handler can then get the current selection by calling the `getSelection` function: + ```js handleSelectionChange(event) { const selection = event.target.getSelection(); @@ -91,29 +98,30 @@ handleSelectionChange(event) { That list contains a maximum of one elements if the lookup is a single entry lookup. ### Reference -| Attribute | Type | Description | -| --- | --- | --- | -| `label` | String | Lookup label | -| `selection` | Array | Lookup initial selection if any | -| `placeholder` | String | Lookup placeholder | -| `isMultiEntry` | Boolean | Whether the lookup is single (default) or multi entry. | -| `errors` | Array | List of errors that are displayed under the lookup. | -| `scrollAfterNItems` | Number | A null or integer value used to force overflow scroll on the result listbox after N number of items. Valid values are null, 5, 7, or 10. Use null to disable overflow scrolling. | -| `customKey` | String | Custom key that can be used to identify this lookup when placed in a collection of similar components. | - -| Function | Description | -| --- | --- | -| `setSearchResults(results)` | Passes a search results array back to the lookup so that they are displayed in the dropdown. | -| `getSelection()` | Gets the current lookup selection. | -| `getkey()` | Retrieves the value of the `customKey` attribute. | -| Event | Description | Data | -| --- | --- | --- | -| `search` | Event fired when a search needs to be performed on the server-side. | `{ searchTerm: String, selectedIds: Array }` | -| `selectionchange` | Event fired when the selection of the lookup changes. This event holds no data, use the `getSelection` function to retrieve the current selection. | none | +| Attribute | Type | Description | +| ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `label` | String | Lookup label | +| `selection` | Array | Lookup initial selection if any | +| `placeholder` | String | Lookup placeholder | +| `isMultiEntry` | Boolean | Whether the lookup is single (default) or multi entry. | +| `errors` | Array | List of errors that are displayed under the lookup. | +| `scrollAfterNItems` | Number | A null or integer value used to force overflow scroll on the result listbox after N number of items. Valid values are null, 5, 7, or 10. Use null to disable overflow scrolling. | +| `customKey` | String | Custom key that can be used to identify this lookup when placed in a collection of similar components. | + +| Function | Description | +| --------------------------- | -------------------------------------------------------------------------------------------- | +| `setSearchResults(results)` | Passes a search results array back to the lookup so that they are displayed in the dropdown. | +| `getSelection()` | Gets the current lookup selection. | +| `getkey()` | Retrieves the value of the `customKey` attribute. | +| Event | Description | Data | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| `search` | Event fired when a search needs to be performed on the server-side. | `{ searchTerm: String, selectedIds: Array }` | +| `selectionchange` | Event fired when the selection of the lookup changes. This event holds no data, use the `getSelection` function to retrieve the current selection. | none | ## Sample application + The default installation installs the lookup component and a sample application available under this URL (replace the domain):
https://<YOUR_DOMAIN>.lightning.force.com/c/SampleLookupApp.app diff --git a/src-sample/main/default/aura/SampleLookupAppTemplate/SampleLookupAppTemplate.cmp b/src-sample/main/default/aura/SampleLookupAppTemplate/SampleLookupAppTemplate.cmp index fe3755a..6c54299 100644 --- a/src-sample/main/default/aura/SampleLookupAppTemplate/SampleLookupAppTemplate.cmp +++ b/src-sample/main/default/aura/SampleLookupAppTemplate/SampleLookupAppTemplate.cmp @@ -1,6 +1,11 @@ - + - + - \ No newline at end of file + diff --git a/src-sample/main/default/classes/SampleLookupController.cls b/src-sample/main/default/classes/SampleLookupController.cls index 8049739..76518d3 100644 --- a/src-sample/main/default/classes/SampleLookupController.cls +++ b/src-sample/main/default/classes/SampleLookupController.cls @@ -1,33 +1,55 @@ public with sharing class SampleLookupController { - private final static Integer MAX_RESULTS = 5; @AuraEnabled(Cacheable=true) - public static List search(String searchTerm, List selectedIds) { + public static List search( + String searchTerm, + List selectedIds + ) { // Prepare query paramters searchTerm += '*'; // Execute search query - List> searchResults = [FIND :searchTerm IN ALL FIELDS RETURNING - Account (Id, Name, BillingCity WHERE id NOT IN :selectedIds), - Opportunity (Id, Name, StageName WHERE id NOT IN :selectedIds) - LIMIT :MAX_RESULTS]; + List> searchResults = [ + FIND :searchTerm + IN ALL FIELDS + RETURNING + Account(Id, Name, BillingCity WHERE id NOT IN :selectedIds), + Opportunity(Id, Name, StageName WHERE id NOT IN :selectedIds) + LIMIT :MAX_RESULTS + ]; // Prepare results List results = new List(); // Extract Accounts & convert them into LookupSearchResult String accountIcon = 'standard:account'; - Account [] accounts = ((List) searchResults[0]); + Account[] accounts = ((List) searchResults[0]); for (Account account : accounts) { - results.add(new LookupSearchResult(account.Id, 'Account', accountIcon, account.Name, 'Account • '+ account.BillingCity)); + results.add( + new LookupSearchResult( + account.Id, + 'Account', + accountIcon, + account.Name, + 'Account • ' + account.BillingCity + ) + ); } // Extract Opportunities & convert them into LookupSearchResult String opptyIcon = 'standard:opportunity'; - Opportunity [] opptys = ((List) searchResults[1]); + Opportunity[] opptys = ((List) searchResults[1]); for (Opportunity oppty : opptys) { - results.add(new LookupSearchResult(oppty.Id, 'Opportunity', opptyIcon, oppty.Name, 'Opportunity • '+ oppty.StageName)); + results.add( + new LookupSearchResult( + oppty.Id, + 'Opportunity', + opptyIcon, + oppty.Name, + 'Opportunity • ' + oppty.StageName + ) + ); } return results; diff --git a/src-sample/main/default/classes/SampleLookupControllerTest.cls b/src-sample/main/default/classes/SampleLookupControllerTest.cls index 1285f73..d7174c1 100644 --- a/src-sample/main/default/classes/SampleLookupControllerTest.cls +++ b/src-sample/main/default/classes/SampleLookupControllerTest.cls @@ -1,20 +1,23 @@ @isTest public class SampleLookupControllerTest { static testMethod void search_should_return_Account() { - Id [] fixedResults = new Id[1]; + Id[] fixedResults = new List(1); Account account = createTestAccount('Account'); fixedResults.add(account.Id); Test.setFixedSearchResults(fixedResults); List selectedIds = new List(); - List results = SampleLookupController.search('Acc', selectedIds); + List results = SampleLookupController.search( + 'Acc', + selectedIds + ); System.assertEquals(1, results.size()); System.assertEquals(account.Id, results.get(0).getId()); } static testMethod void search_should_not_return_selected_item() { - Id [] fixedResults = new Id[1]; + Id[] fixedResults = new List(1); Account account1 = createTestAccount('Account1'); fixedResults.add(account1.Id); Account account2 = createTestAccount('Account2'); @@ -23,7 +26,10 @@ public class SampleLookupControllerTest { List selectedIds = new List(); selectedIds.add(account2.Id); - List results = SampleLookupController.search('Acc', selectedIds); + List results = SampleLookupController.search( + 'Acc', + selectedIds + ); System.assertEquals(1, results.size()); System.assertEquals(account1.Id, results.get(0).getId()); diff --git a/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.html b/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.html index 807da72..730cc8d 100644 --- a/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.html +++ b/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.html @@ -1,16 +1,29 @@ \ No newline at end of file + diff --git a/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.js b/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.js index 4c0b128..0dc3599 100644 --- a/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.js +++ b/src-sample/main/default/lwc/sampleLookupContainer/sampleLookupContainer.js @@ -5,13 +5,18 @@ import { ShowToastEvent } from 'lightning/platformShowToastEvent'; import apexSearch from '@salesforce/apex/SampleLookupController.search'; export default class SampleLookupContainer extends LightningElement { - // Use alerts instead of toast to notify user @api notifyViaAlerts = false; - + @track isMultiEntry = false; @track initialSelection = [ - {id: 'na', sObjectType: 'na', icon: 'standard:lightning_component', title: 'Inital selection', subtitle:'Not a valid record'} + { + id: 'na', + sObjectType: 'na', + icon: 'standard:lightning_component', + title: 'Inital selection', + subtitle: 'Not a valid record' + } ]; @track errors = []; @@ -24,10 +29,16 @@ export default class SampleLookupContainer extends LightningElement { handleSearch(event) { apexSearch(event.detail) .then(results => { - this.template.querySelector('c-lookup').setSearchResults(results); + this.template + .querySelector('c-lookup') + .setSearchResults(results); }) .catch(error => { - this.notifyUser('Lookup Error', 'An error occured while searching with the lookup field.', 'error'); + this.notifyUser( + 'Lookup Error', + 'An error occured while searching with the lookup field.', + 'error' + ); // eslint-disable-next-line no-console console.error('Lookup error', JSON.stringify(error)); this.errors = [error]; @@ -46,7 +57,9 @@ export default class SampleLookupContainer extends LightningElement { } checkForErrors() { - const selection = this.template.querySelector('c-lookup').getSelection(); + const selection = this.template + .querySelector('c-lookup') + .getSelection(); if (selection.length === 0) { this.errors = [ { message: 'You must make a selection before submitting!' }, @@ -58,7 +71,7 @@ export default class SampleLookupContainer extends LightningElement { } notifyUser(title, message, variant) { - if (this.notifyViaAlerts){ + if (this.notifyViaAlerts) { // Notify via alert // eslint-disable-next-line no-alert alert(`${title}\n${message}`); @@ -68,4 +81,4 @@ export default class SampleLookupContainer extends LightningElement { this.dispatchEvent(toastEvent); } } -} \ No newline at end of file +} diff --git a/src/main/default/classes/LookupSearchResult.cls b/src/main/default/classes/LookupSearchResult.cls index e37ed11..03715c5 100644 --- a/src/main/default/classes/LookupSearchResult.cls +++ b/src/main/default/classes/LookupSearchResult.cls @@ -1,16 +1,21 @@ /** -* Class used to serialize a single Lookup search result item -* The Lookup controller returns a List when sending search result back to Lightning -*/ + * Class used to serialize a single Lookup search result item + * The Lookup controller returns a List when sending search result back to Lightning + */ public class LookupSearchResult { - private Id id; private String sObjectType; private String icon; private String title; private String subtitle; - public LookupSearchResult(Id id, String sObjectType, String icon, String title, String subtitle) { + public LookupSearchResult( + Id id, + String sObjectType, + String icon, + String title, + String subtitle + ) { this.id = id; this.sObjectType = sObjectType; this.icon = icon; diff --git a/src/main/default/lwc/lookup/__tests__/lookupEventFiring.test.js b/src/main/default/lwc/lookup/__tests__/lookupEventFiring.test.js index 04ad47b..8707a52 100644 --- a/src/main/default/lwc/lookup/__tests__/lookupEventFiring.test.js +++ b/src/main/default/lwc/lookup/__tests__/lookupEventFiring.test.js @@ -28,7 +28,7 @@ describe('c-lookup event fires', () => { it('search event fires', () => { jest.useFakeTimers(); - + // Create element with mock search handler const mockSearchFn = jest.fn(); const element = createElement('c-lookup', { @@ -36,7 +36,7 @@ describe('c-lookup event fires', () => { }); element.addEventListener('search', mockSearchFn); element.isMultiEntry = true; - element.selection = SAMPLE_SEARCH_ITEMS; + element.selection = SAMPLE_SEARCH_ITEMS; document.body.appendChild(element); // Set search term and force input change @@ -55,4 +55,4 @@ describe('c-lookup event fires', () => { selectedIds: ['id1', 'id2'] }); }); -}); \ No newline at end of file +}); diff --git a/src/main/default/lwc/lookup/__tests__/lookupEventHandling.test.js b/src/main/default/lwc/lookup/__tests__/lookupEventHandling.test.js index 7f866cd..4e27529 100644 --- a/src/main/default/lwc/lookup/__tests__/lookupEventHandling.test.js +++ b/src/main/default/lwc/lookup/__tests__/lookupEventHandling.test.js @@ -16,7 +16,6 @@ const SAMPLE_SEARCH_ITEMS = [ } ]; - describe('c-lookup event handling', () => { afterEach(() => { // The jsdom instance is shared across test cases in a single file so reset the DOM @@ -31,7 +30,7 @@ describe('c-lookup event handling', () => { is: Lookup }); element.isMultiEntry = false; - element.selection = [ SAMPLE_SEARCH_ITEMS[0] ]; + element.selection = [SAMPLE_SEARCH_ITEMS[0]]; document.body.appendChild(element); // Clear selection diff --git a/src/main/default/lwc/lookup/__tests__/lookupExposedFunctions.test.js b/src/main/default/lwc/lookup/__tests__/lookupExposedFunctions.test.js index f9abc23..761397e 100644 --- a/src/main/default/lwc/lookup/__tests__/lookupExposedFunctions.test.js +++ b/src/main/default/lwc/lookup/__tests__/lookupExposedFunctions.test.js @@ -16,7 +16,6 @@ const SAMPLE_SEARCH_ITEMS = [ } ]; - describe('c-lookup exposed functions', () => { afterEach(() => { // The jsdom instance is shared across test cases in a single file so reset the DOM diff --git a/src/main/default/lwc/lookup/__tests__/lookupRendering.test.js b/src/main/default/lwc/lookup/__tests__/lookupRendering.test.js index c9545ff..bec103c 100644 --- a/src/main/default/lwc/lookup/__tests__/lookupRendering.test.js +++ b/src/main/default/lwc/lookup/__tests__/lookupRendering.test.js @@ -1,7 +1,6 @@ import { createElement } from 'lwc'; import Lookup from 'c/lookup'; - describe('c-lookup rendering', () => { afterEach(() => { // The jsdom instance is shared across test cases in a single file so reset the DOM @@ -50,7 +49,9 @@ describe('c-lookup rendering', () => { const clearSelButton = element.shadowRoot.querySelector('button'); expect(clearSelButton.title).toBe('Remove selected option'); // Verify result list is NOT rendered - const selList = element.shadowRoot.querySelectorAll('ul.slds-listbox_inline'); + const selList = element.shadowRoot.querySelectorAll( + 'ul.slds-listbox_inline' + ); expect(selList.length).toBe(0); }); @@ -69,7 +70,9 @@ describe('c-lookup rendering', () => { const clearSelButton = element.shadowRoot.querySelectorAll('button'); expect(clearSelButton.length).toBe(0); // Verify result list is rendered - const selList = element.shadowRoot.querySelectorAll('ul.slds-listbox_inline'); + const selList = element.shadowRoot.querySelectorAll( + 'ul.slds-listbox_inline' + ); expect(selList.length).toBe(1); }); @@ -79,8 +82,8 @@ describe('c-lookup rendering', () => { is: Lookup }); element.errors = [ - {id: 'e1', message: 'Sample error 1'}, - {id: 'e2', message: 'Sample error 2'} + { id: 'e1', message: 'Sample error 1' }, + { id: 'e2', message: 'Sample error 2' } ]; document.body.appendChild(element); @@ -89,4 +92,4 @@ describe('c-lookup rendering', () => { expect(errors.length).toBe(2); expect(errors[0].textContent).toBe('Sample error 1'); }); -}); \ No newline at end of file +}); diff --git a/src/main/default/lwc/lookup/lookup.css b/src/main/default/lwc/lookup/lookup.css index a81605b..bf550aa 100644 --- a/src/main/default/lwc/lookup/lookup.css +++ b/src/main/default/lwc/lookup/lookup.css @@ -1,6 +1,6 @@ .slds-combobox__input, .slds-combobox_container { - transition: border .1s linear, box-shadow .1 linear; + transition: border 0.1s linear, box-shadow 0.1 linear; } .slds-combobox__input { @@ -27,4 +27,4 @@ .form-error { color: rgb(194, 57, 52); display: block; -} \ No newline at end of file +} diff --git a/src/main/default/lwc/lookup/lookup.html b/src/main/default/lwc/lookup/lookup.html index 04ea935..76ae2e0 100644 --- a/src/main/default/lwc/lookup/lookup.html +++ b/src/main/default/lwc/lookup/lookup.html @@ -3,26 +3,50 @@
-
- +
- - + - + -