├── LICENSE
├── README.md
└── src
├── applications
└── Service_Components.app
├── aura
├── AccountSelector
│ ├── AccountSelector.cmp
│ ├── AccountSelector.cmp-meta.xml
│ ├── AccountSelectorController.js
│ └── AccountSelectorHelper.js
├── CaseDatatable
│ ├── CaseDatatable.cmp
│ ├── CaseDatatable.cmp-meta.xml
│ ├── CaseDatatableController.js
│ └── CaseDatatableHelper.js
├── ContactAddressForm
│ ├── ContactAddressForm.cmp
│ ├── ContactAddressForm.cmp-meta.xml
│ ├── ContactAddressForm.css
│ ├── ContactAddressFormController.js
│ └── ContactAddressFormHelper.js
├── ContactDatatable
│ ├── ContactDatatable.cmp
│ ├── ContactDatatable.cmp-meta.xml
│ ├── ContactDatatableController.js
│ └── ContactDatatableHelper.js
├── DataService
│ ├── DataService.cmp
│ ├── DataService.cmp-meta.xml
│ ├── DataServiceController.js
│ └── DataServiceHelper.js
├── DataTableService
│ ├── DataTableService.cmp
│ ├── DataTableService.cmp-meta.xml
│ ├── DataTableServiceController.js
│ └── DataTableServiceHelper.js
├── EventService
│ ├── EventService.cmp
│ ├── EventService.cmp-meta.xml
│ ├── EventServiceController.js
│ └── EventServiceHelper.js
├── MessageService
│ ├── MessageService.cmp
│ ├── MessageService.cmp-meta.xml
│ ├── MessageServiceController.js
│ └── MessageServiceHelper.js
├── PlatformEventListener
│ ├── PlatformEventListener.cmp
│ ├── PlatformEventListener.cmp-meta.xml
│ ├── PlatformEventListener.css
│ ├── PlatformEventListenerController.js
│ └── PlatformEventListenerHelper.js
├── QuickUpdateService
│ ├── QuickUpdateService.cmp
│ ├── QuickUpdateService.cmp-meta.xml
│ ├── QuickUpdateServiceController.js
│ └── QuickUpdateServiceHelper.js
├── ServiceAppEvent
│ ├── ServiceAppEvent.evt
│ └── ServiceAppEvent.evt-meta.xml
├── ServiceCompEvent
│ ├── ServiceCompEvent.evt
│ └── ServiceCompEvent.evt-meta.xml
├── ServiceRecordEvent
│ ├── ServiceRecordEvent.evt
│ └── ServiceRecordEvent.evt-meta.xml
├── modalFooter
│ ├── modalFooter.cmp
│ ├── modalFooter.cmp-meta.xml
│ └── modalFooterController.js
└── onMessage
│ ├── onMessage.evt
│ └── onMessage.evt-meta.xml
├── classes
├── DataServiceCtrl.cls
├── DataServiceCtrl.cls-meta.xml
├── DataTableService.cls
└── DataTableService.cls-meta.xml
├── flexipages
├── Service_Components_Hello_World.flexipage
└── Service_Components_UtilityBar.flexipage
├── flows
└── Contacts_All.flow
├── objects
└── Contact_DML__e.object
├── package.xml
├── profiles
└── Admin.profile
└── tabs
└── SC_Sample_App.tab
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2017, James H
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Service Component Design Pattern
2 |
3 | Lighting Web Components addresses many of the issues this design pattern sought to address, so moving forward I will be maintaining only the [lwc-utils repo](https://github.com/tsalb/lwc-utils) where you can find LWC specific design patterns (and utility modules) compared side by side with this deprecated aura service component framework.
4 |
5 | ~~The Service Component design pattern makes it easy for custom components placed separately from each other (not having any parent-child hierarchy) to easily share a single Apex Controller and reduce redundancy of inter-component communication through key-value events.~~
6 |
7 | ~~Having no component hierarchy makes it more simple to place components anywhere on a lightning page and allowing more flexibility of creating a dynamic user experience out of mixing native and custom components.~~
8 |
9 | ~~Additionally, the Service Component design pattern allows wrapping of base lightning components to provide much more utility than what is currently offered.~~
10 |
11 | ~~**tl;dr: Deploy > App Launcher > Service Components**~~
12 |
13 |
14 |
16 |
17 |
18 | ---
19 |
20 | The service components in this sample app are:
21 |
22 | `DataService` which encapsulates serverside callouts. A single Apex Controller is attributed to this headless component which uses `aura:methods` to pass parameters to the JS controller which handles serverside configuration like `action.setStorable()` or `action.setParams()`. The action will be passed to `helper.dispatch()` to make the asynchronous callout.
23 |
24 | `EventService` which encapsulates a key-value pair (optional value) model for both application and component events. This component registers and fires generic events which need to be parsed by the handling component(s) via key-value. There is a special recordEvent which is for Lightning Console (since app events broadcast to all console tabs). This can also listen to Platform Events easily and react to them (via `lightning:empApi`).
25 |
26 | `MessageService` which wraps `lightning:overlayLibrary` and provides dynamic creation `aura:methods` for modal bodies and footers.
27 |
28 | `QuickUpdateService` which wraps Lightning Data Service (i.e. `force:recordData`) and provides an `aura:method` to very quickly configure a single-object, single-record DML to any sObject. Since this uses LDS, profile security is respected. This is a POC component.
29 |
30 | `DataTableService` which can quickly generate `tableData` and `tableColumns` in a format expected by `lightning:datatable`. It's designed primarily for read-only, single hierarchy tables. It's still possible to perform further processing either serverside or clientside to configure `lightning:datatable` more granularly. Parent relationship (1 level tested) works and there are helper functions to flatten the data both serverside (column definition) and clientside (data definition).
31 |
32 | ---
33 |
34 | ## DataService Usage Example
35 | Drop this into a component that needs serverside data:
36 |
37 | **AccountSelector.cmp**
38 | ```xml
39 |
40 |
41 |
42 |
43 | ```
44 |
45 | **AccountSelectorController.js**
46 | ```javascript
47 | doInit: function (component, event, helper) {
48 | helper.service(component).fetchAccountCombobox(
49 | $A.getCallback((error, data) => {
50 | if (data) {
51 | console.log("data from my apex controller is: "+data);
52 | }
53 | })
54 | );
55 | },
56 | ```
57 | **AccountSelectorHelper.js**
58 | ```javascript
59 | // ServiceHeaderHelper.js
60 | service : function(component) {
61 | return component.find("service");
62 | },
63 | ```
64 |
65 | ## EventService Usage Examples
66 | Some samples from the app:
67 | ```javascript
68 |
69 | helper.eventService(component).fireAppEvent("ACCOUNT_ID_SELECTED", selectedOptionValue);
70 |
71 | helper.eventService(component).fireAppEvent("HEADER_CLEARTABLE");
72 |
73 | ```
74 |
75 | ## Handling App, Record, or Comp events with EventService
76 | In any component that needs to listen to these, attach a handler like this:
77 |
78 | **ContactDatatable.cmp**
79 | ```xml
80 |
81 | ```
82 | **ContactDatatableController.js**
83 | ```javascript
84 | handleApplicationEvent : function(component, event, helper) {
85 | let params = event.getParams();
86 | switch(params.appEventKey) {
87 | case "ACCOUNT_ID_SELECTED": // fallthrough
88 | case "CONTACTS_UPDATED":
89 | helper.loadContactTable(component, params.appEventValue);
90 | break;
91 | case "HEADER_CLEARTABLE":
92 | component.set("v.tableData", null);
93 | break;
94 | }
95 | },
96 | ```
97 |
98 | ## Handling Platform Events with EventService
99 | Using v44, we can leverage `lightning:empApi` to do this:
100 |
101 | **PlatformEventListener.cmp**
102 | ```xml
103 |
106 | ```
107 | **PlatformEventListenerController.js**
108 | ```javascript
109 | handleContactDmlEvent : function(component, event, helper) {
110 | let payloadJSON = JSON.stringify(event.getParam("payload"));
111 | component.set("v.payloadJSON", payloadJSON);
112 | }
113 | ```
114 |
115 | ## MessageService Usage Examples
116 | At its core, this is a wrapper around the lightning:overlayLibrary which provides some helper functionality for creating both the body and the footer. There are some special features:
117 |
118 | - Able to handle text or custom component as the modal body.
119 | - Always handles the footer cancel button.
120 | - Specify a main action function which can be either:
121 | - On the originating component by using `component.getReference("someFunction")`.
122 | - On the modal component (the body) that's being created by using `"c.someFunctionOnTheModalComponent"`.
123 | - Pass an Object of parameters to the modal component (the body) from the originating component by using object notation while setting up the modal.
124 |
125 | When you drop in `MessageService.cmp` into a component, such as `ContactDatatable.cmp`, this is an example of how you can open a modal from a function in `ContactDatatableController.js`.
126 |
127 | **ContactDatatableController.js**
128 | ```javascript
129 | handleOpenComponentModal : function(component, event, helper) {
130 | let selectedArr = component.find("searchTable").getSelectedRows();
131 |
132 | helper.messageService(component).modal(
133 | "update-address-modal", // auraId
134 | "Update Address: "+selectedArr.length+" Row(s)", // headerLabel
135 | "c:ContactAddressForm", // body, MessageService will dynamically create this
136 | {
137 | contactList: selectedArr // bodyParams, MessageService dynamically passes these to c:ContactAddressForm
138 | },
139 | "c.handleUpdateMultiAddress", // mainActionReference, see above on where you can feed this
140 | "Update" // mainActionLabel
141 | );
142 | },
143 | ```
144 |
145 | The above `c.handleUpdateMultiAddress` is a reference to a function found on `ContactAddressForm.cmp`. `MessageService.cmp` is able to grab reference appropriately and wire it up to the `Update` main action found in the modal footer.
146 |
147 | So, even though overlayLibrary `modalBody` and `modalFooter` are siblings, the footer is referencing a controller action on the body. This makes it easier to write all your container logic on a `modalBody` and leverage `MessageService.cmp` to just open a self-contained `modalBody` component.
148 |
149 |
150 | ## QuickUpdateService Usage Examples
151 | At its core, this is a wrapper around force:recordData which allows for simple single record DML.
152 |
153 | This example from `ContactDatatable.cmp` uses a single button to update multiple fields on a single record. The only attributes `QuickUpdateService.cmp` expects is a `configObject` containing the `recordId` and `fieldUpdates` properties.
154 |
155 | Currently, there is no type checking or much error handling.
156 |
157 | **ContactDatatableHelper.js**
158 | ```javascript
159 | clearMailingAddressWithLightningDataService : function(component, row) {
160 | let _self = this;
161 | let configObject = { // QuickUpdateService only expects this object with recordId and fieldUpdates properties
162 | recordId: row["Id"],
163 | fieldUpdates: {
164 | "MailingStreet": null,
165 | "MailingCity": null,
166 | "MailingState": null,
167 | "MailingPostalCode": null,
168 | "MailingCountry": null
169 | }
170 | }
171 | _self.quickUpdateService(component).LDS_Update(
172 | configObject,
173 | $A.getCallback((saveResult) => {
174 | switch(saveResult.state.toUpperCase()) {
175 | case "SUCCESS":
176 | _self.messageService(component).showToast({
177 | message: "Cleared Mailing Address.",
178 | variant: "success"
179 | });
180 | _self.loadContactTable(component, row["AccountId"]);
181 | break;
182 | case "ERROR":
183 | _self.messageService(component).showToast({
184 | title: "Error Clearing Mailing Address",
185 | message: JSON.stringify(saveResult.error[0].message),
186 | variant: "error",
187 | mode: "pester"
188 | });
189 | break;
190 | }
191 | })
192 | );
193 | },
194 | ```
195 |
196 |
197 | ## DataTableService Usage Examples
198 | This is a library service component. It's designed to make read-only lightning:datatable very quick to spin up.
199 |
200 | This example is from `CaseDatatable.cmp` (which is actually created inside a modal from `ContactDatatable.cmp`). The only attributes `DataTableService.cmp` expects is a `tableRequest` Object containing the `queryString` and `bindVars` properties.
201 |
202 | There is no way to fetch the more granular `tableColumns` specific configurations that are offered from `lightning:datatable` however it's possible to post-process the `tableColumns` data even futher serverside OR clientside.
203 |
204 | There is simple handling of parent relationships fields.
205 |
206 | **CaseDatatableController.js**
207 | ```javascript
208 | doInit : function(component, event, helper) {
209 | let contactRecordId = [].concat(component.get("v.contactRecordId")); // guarantees array for idSet
210 | if (!$A.util.isEmpty(contactRecordId)) {
211 | let tableRequest = {
212 | queryString: "SELECT "
213 | + "Id, CaseNumber, CreatedDate, ClosedDate, Description, Comments, Status, Subject, Type, Owner.Name"
214 | + "FROM Case "
215 | + "WHERE ContactId =: idSet "
216 | + "ORDER BY CaseNumber ASC",
217 | bindVars: {
218 | idSet: contactRecordId,
219 | }
220 | }
221 | helper.tableService(component).fetchData(
222 | tableRequest,
223 | $A.getCallback((error, data) => {
224 | if (!$A.util.isEmpty(data)) {
225 | component.set("v.tableData", data.tableData);
226 | component.set("v.tableColumns", data.tableColumns);
227 | } else {
228 | if (!$A.util.isEmpty(error) && error[0].hasOwnProperty("message")) {
229 | helper.messageService(component).showToast({
230 | message: error[0].message,
231 | variant: "error"
232 | });
233 | }
234 | }
235 | })
236 | );
237 | }
238 | }
239 | ```
240 |
--------------------------------------------------------------------------------
/src/applications/Service_Components.app:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #0070D2
5 | false
6 |
7 | github.com/tsalb/sfdc-lightning-service-components
8 | Large
9 | false
10 | false
11 |
12 | Standard
13 | SC_Sample_App
14 | Lightning
15 | Service_Components_UtilityBar
16 |
17 |
--------------------------------------------------------------------------------
/src/aura/AccountSelector/AccountSelector.cmp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Top Accounts with Contacts
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/aura/AccountSelector/AccountSelector.cmp-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 43.0
4 | Select from a limited set of Accounts
5 |
6 |
--------------------------------------------------------------------------------
/src/aura/AccountSelector/AccountSelectorController.js:
--------------------------------------------------------------------------------
1 | ({
2 | doInit: function (component, event, helper) {
3 | helper.service(component).fetchAccountCombobox(
4 | $A.getCallback((error, data) => {
5 | // This returns whatever datatype is specified in the controller
6 | if (!$A.util.isEmpty(data)) {
7 | component.set("v.topAccounts", JSON.parse(data).items);
8 | } else {
9 | helper.messageService(component).showToast({
10 | message: "No Accounts in org!",
11 | variant: "error"
12 | });
13 | }
14 | })
15 | );
16 | },
17 | handleAccountOptionSelected : function(component, event, helper) {
18 | helper.eventService(component).fireAppEvent("ACCOUNT_ID_SELECTED", event.getParam("value"));
19 | },
20 | handleClearTableOnly : function(component, event, helper) {
21 | helper.eventService(component).fireAppEvent("HEADER_CLEARTABLE");
22 | },
23 | })
--------------------------------------------------------------------------------
/src/aura/AccountSelector/AccountSelectorHelper.js:
--------------------------------------------------------------------------------
1 | ({
2 | service : function(component) {
3 | return component.find("service");
4 | },
5 | messageService : function(component) {
6 | return component.find("messageService");
7 | },
8 | eventService : function(component) {
9 | return component.find("eventService");
10 | },
11 | })
--------------------------------------------------------------------------------
/src/aura/CaseDatatable/CaseDatatable.cmp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |