├── .forceignore ├── .gitignore ├── README.md ├── config └── project-scratch-def.json ├── force-app └── main │ └── default │ ├── aura │ └── URL_CreateRecordCmp │ │ ├── URL_CreateRecordCmp.cmp │ │ ├── URL_CreateRecordCmp.cmp-meta.xml │ │ ├── URL_CreateRecordCmpController.js │ │ └── URL_CreateRecordCmpHelper.js │ └── classes │ ├── URL_CreateRecordController.cls │ ├── URL_CreateRecordController.cls-meta.xml │ ├── URL_CreateRecordControllerTest.cls │ └── URL_CreateRecordControllerTest.cls-meta.xml ├── images ├── cancel_save_button_behavior.gif ├── contact_url_hack_cmp.png └── record-create-url-cmp-cancel.png └── sfdx-project.json /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sfdx 2 | .idea 3 | .vscode 4 | IlluminatedCloud -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Legitimate "URL Hacking" in Lightning Experience 2 | ================================================ 3 | 4 | ![screen shot](images/contact_url_hack_cmp.png) 5 | 6 | Inspired by [Brian Kwong](https://twitter.com/Kwongerific)'s [blog post](https://thewizardnews.com/2018/08/02/url-hack-functionality-lightning/) 7 | on "URL Hacking" with supported features in Lightning Experience. 8 | 9 | This project's purpose is to determine a way to provide the "URL Hacking" in Lightning Experience 10 | with supported features in Lightning Experience but without the need of a Flow so that we can have 11 | a reusable solution for any object and supporting arbitrary number of fields and data types. 12 | This is actually one of the wish items Brian talks about at the end of his blog post. 13 | 14 | Creating Custom Button URL 15 | -------------------------- 16 | 17 | The start of the URL should be `/lightning/cmp/c__URL_CreateRecordCmp?`, this tells Lightning Experience which component to navigate to. 18 | The below table describes the supported parameters to append to the URL. Separate each `parameter=value` pair with `&`. 19 | 20 | | Name | Description | Required? | 21 | |------|-------------|-----------| 22 | | `objectName` | API name of the object whose create form to show. | Yes | 23 | | `recordTypeId` | ID of the record type of the new record. | No | 24 | | `recordId` | ID of the current record where user is clicking the button from. Workaround for no cancel behavior customization. | No | 25 | | field name | One or more API field names whose default values to populate (e.g. FirstName, Phone, Active__c) | No | 26 | 27 | 28 | Examples 29 | -------- 30 | 31 | Account button that opens the new contact form, pre-populating the first and last name fields: 32 | * `/lightning/cmp/c__URL_CreateRecordCmp?recordId={!Account.Id}&objectName=Contact&FirstName=Doug&LastName=Ayers` 33 | 34 | Design 35 | ------ 36 | 37 | In [Summer '18](https://releasenotes.docs.salesforce.com/en-us/summer18/release-notes/rn_lc_components_navigation.htm), 38 | a new interface was introduced, [lightning:isUrlAddressable](https://developer.salesforce.com/docs/component-library/bundle/lightning:isUrlAddressable/documentation), 39 | which allows us to create URLs and pass arbitrary parameters to Lightning Components. Great! So that part is done, no 40 | need for Flow, we can create URL buttons similar to how we did in Classic so many moons ago. 41 | 42 | However, by navigating to this lightning component as a URL, you are navigating away from the original 43 | record page you were on when you clicked the button. The problem with that is there is no way to control 44 | what happens when the user clicks the **cancel** or **save** buttons in the modal dialog. 45 | 46 | When the user clicks the **save** button, then they are taken to view the new record. 47 | This default behavior is acceptable, for now. 48 | 49 | When the user clicks the **cancel** button, though, the modal simply disappears. 50 | The user is not redirected back to where they had clicked the button, so they are left with a blank page. 51 | This default behavior is not acceptable. 52 | 53 | ![screen shot](images/record-create-url-cmp-cancel.png) 54 | 55 | To compensate for the lack of control of the cancel behavior, the component supports a `recordId` URL parameter. 56 | When specified in the URL, the record is loaded in the background when the create record modal appears. By the time the user clicks the 57 | cancel button, the original record will have been loaded and the user won't notice that they had navigated away to begin with. 58 | 59 | ![screen shot](images/cancel_save_button_behavior.gif) 60 | 61 | Next Steps 62 | ---------- 63 | 64 | Please vote for the `force:recordCreate` event to expose callbacks to customize the modal dialog button behavior: 65 | * [Callback method for force:createRecord event to redirect or refresh after save](https://success.salesforce.com/ideaView?id=0873A0000003V4hQAE) 66 | * [Allow redirect after creating a new record using force:createRecord](https://success.salesforce.com/ideaView?id=0873A0000003VnmQAE) 67 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName" : "Company", 3 | "edition" : "Developer", 4 | "settings" : { 5 | "orgPreferenceSettings" : { 6 | "s1DesktopEnabled" : true, 7 | "s1EncryptedStoragePref2" : false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmp.cmp: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmp.cmp-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45.0 4 | URL_CreateRecordCmp 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmpController.js: -------------------------------------------------------------------------------- 1 | ({ 2 | // this method is called when component initializes 3 | onInit: function( component, event, helper ) { 4 | helper.handleShowCreateForm( component ); 5 | } 6 | }) -------------------------------------------------------------------------------- /force-app/main/default/aura/URL_CreateRecordCmp/URL_CreateRecordCmpHelper.js: -------------------------------------------------------------------------------- 1 | ({ 2 | handleShowCreateForm: function( component ) { 3 | 4 | /* 5 | * Supported URL parameters: 6 | * objectName - API name of a standard or custom object (e.g. Account or Project__c) 7 | * recordTypeId - which record type to use, if not specified then the user's default record type is assumed 8 | * 9 | * All other key=value URL parameters are 10 | * assumed as default field values for the record form. 11 | * 12 | * URL parameter names must match the API field names exactly (case-sensitive). 13 | * For example, "phone" and "PHONE" will not pre-populate the standard Contact "Phone" field. 14 | * 15 | * Example Custom Button URL: 16 | * "/lightning/cmp/c__URL_CreateRecordCmp?objectName=Contact&FirstName=Astro&LastName=Nomical&AccountId={!Account.Id}" 17 | */ 18 | 19 | let helper = this; 20 | 21 | let pageRef = component.get( 'v.pageReference' ); 22 | 23 | // Retrieve specific parameters from the URL. 24 | // For case-insensitivity, the properties are lowercase. 25 | let urlParamMap = { 26 | 'objectname' : '', // object whose create form to display 27 | 'recordtypeid' : '', // record type for new record (optional) 28 | 'recordid' : '' // id of record where button was clicked 29 | }; 30 | 31 | for ( let key in pageRef.state ) { 32 | let lowerKey = key.toLowerCase(); 33 | if ( urlParamMap.hasOwnProperty( lowerKey ) ) { 34 | urlParamMap[lowerKey] = pageRef.state[key]; 35 | } 36 | } 37 | 38 | console.log( 'urlParamMap', urlParamMap ); 39 | 40 | Promise.resolve() 41 | .then( function() { 42 | if ( !$A.util.isEmpty( urlParamMap.recordid ) ) { 43 | // workaround for not being able to customize the cancel 44 | // behavior of the force:createRecord event. instead of 45 | // the user seeing a blank page, instead load in the background 46 | // the very record the user is viewing so when they click cancel 47 | // they are still on the same record. 48 | helper.navigateToUrl( '/' + urlParamMap.recordid ); 49 | // give the page some time to load the new url 50 | // otherwise we end up firing the show create form 51 | // event too early and the page navigation happens 52 | // afterward, causing the quick action modal to disappear. 53 | return new Promise( function( resolve, reject ) { 54 | setTimeout( resolve, 1000 ); 55 | }); 56 | } 57 | }) 58 | .then( function() { 59 | helper.showCreateForm( component, urlParamMap, pageRef ); 60 | }); 61 | 62 | }, 63 | 64 | // ----------------------------------------------------------------- 65 | 66 | showCreateForm: function( component, urlParamMap, pageRef ) { 67 | 68 | let helper = this; 69 | 70 | helper.enqueueAction( component, 'c.getFieldDescribeMap', { 71 | 72 | 'objectName' : urlParamMap.objectname 73 | 74 | }).then( $A.getCallback( function( fieldDescribeMap ) { 75 | 76 | console.log( 'fieldDescribeMap', fieldDescribeMap ); 77 | 78 | let eventParamMap = { 79 | 'defaultFieldValues' : {} 80 | }; 81 | 82 | if ( !$A.util.isEmpty( urlParamMap.objectname ) ) { 83 | eventParamMap['entityApiName'] = urlParamMap.objectname; 84 | } 85 | 86 | if ( !$A.util.isEmpty( urlParamMap.recordtypeid ) ) { 87 | eventParamMap['recordTypeId'] = urlParamMap.recordtypeid; 88 | } 89 | 90 | // ensure only fields the current user has permission to create are set 91 | // otherwise upon attempt to save will get component error 92 | for ( let fieldName in pageRef.state ) { 93 | if ( fieldDescribeMap.hasOwnProperty( fieldName ) && fieldDescribeMap[fieldName].createable ) { 94 | // avoid setting lookup fields to undefined, get Error ID: 1429293140-211986 (-590823013), assign to null instead 95 | eventParamMap.defaultFieldValues[fieldName] = pageRef.state[fieldName] || null; 96 | } 97 | } 98 | 99 | return eventParamMap; 100 | 101 | })).then( $A.getCallback( function( eventParamMap ) { 102 | 103 | console.log( 'eventParamMap', eventParamMap ); 104 | 105 | $A.get( 'e.force:createRecord' ).setParams( eventParamMap ).fire(); 106 | 107 | })).catch( $A.getCallback( function( err ) { 108 | 109 | helper.logActionErrors( err ); 110 | 111 | })); 112 | 113 | }, 114 | 115 | navigateToUrl: function( url ) { 116 | 117 | console.log( 'navigating to url', url ); 118 | 119 | if ( !$A.util.isEmpty( url ) ) { 120 | $A.get( 'e.force:navigateToURL' ).setParams({ 'url': url }).fire(); 121 | } 122 | 123 | }, 124 | 125 | enqueueAction: function( component, actionName, params, options ) { 126 | 127 | let helper = this; 128 | 129 | return new Promise( function( resolve, reject ) { 130 | 131 | component.set( 'v.showSpinner', true ); 132 | 133 | let action = component.get( actionName ); 134 | 135 | if ( params ) { 136 | action.setParams( params ); 137 | } 138 | 139 | if ( options ) { 140 | if ( options.background ) { action.setBackground(); } 141 | if ( options.storable ) { action.setStorable(); } 142 | } 143 | 144 | action.setCallback( helper, function( response ) { 145 | 146 | component.set( 'v.showSpinner', false ); 147 | 148 | if ( component.isValid() && response.getState() === 'SUCCESS' ) { 149 | 150 | resolve( response.getReturnValue() ); 151 | 152 | } else { 153 | 154 | console.error( 'Error calling action "' + actionName + '" with state: ' + response.getState() ); 155 | 156 | helper.logActionErrors( response.getError() ); 157 | 158 | reject( response.getError() ); 159 | 160 | } 161 | }); 162 | 163 | $A.enqueueAction( action ); 164 | 165 | }); 166 | }, 167 | 168 | logActionErrors : function( errors ) { 169 | if ( errors ) { 170 | if ( errors.length > 0 ) { 171 | for ( let i = 0; i < errors.length; i++ ) { 172 | console.error( 'Error: ' + errors[i].message ); 173 | } 174 | } else { 175 | console.error( 'Error: ' + errors ); 176 | } 177 | } else { 178 | console.error( 'Unknown error' ); 179 | } 180 | } 181 | }) -------------------------------------------------------------------------------- /force-app/main/default/classes/URL_CreateRecordController.cls: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Author: Doug Ayers 4 | * Website: https://douglascayers.com 5 | * GitHub: https://github.com/douglascayers/sfdx-record-create-url-component 6 | * License: BSD 3-Clause License 7 | */ 8 | public class URL_CreateRecordController { 9 | 10 | /** 11 | * Returns basic field describe to know which fields 12 | * are or are not createable and updateable. 13 | * 14 | * @param objectName 15 | * API name of the object whose field describe to return 16 | */ 17 | @AuraEnabled( cacheable = true ) 18 | public static Map getFieldDescribeMap( String objectName ) { 19 | 20 | Map fieldDescribeMap = new Map(); 21 | 22 | // Performance trick to use Type.forName instead of Schema.getGlobalDescribe() 23 | // https://salesforce.stackexchange.com/a/32538/987 24 | // https://salesforce.stackexchange.com/a/219010/987 25 | Type reflector = Type.forName( objectName ); 26 | SObject obj = (SObject) reflector.newInstance(); 27 | SObjectType objType = obj.getSObjectType(); 28 | 29 | DescribeSObjectResult describe = objType.getDescribe(); 30 | Map fieldsMap = describe.fields.getMap(); 31 | for ( String fieldName : fieldsMap.keySet() ) { 32 | DescribeFieldResult fieldDescribe = fieldsMap.get( fieldName ).getDescribe(); 33 | fieldDescribeMap.put( fieldDescribe.getName(), new Map{ 34 | 'accessible' => fieldDescribe.isAccessible(), 35 | 'createable' => fieldDescribe.isCreateable(), 36 | 'updateable' => fieldDescribe.isUpdateable() 37 | }); 38 | } 39 | 40 | return fieldDescribeMap; 41 | } 42 | 43 | } 44 | /* 45 | BSD 3-Clause License 46 | Copyright (c) 2018, Doug Ayers, douglascayers.com 47 | All rights reserved. 48 | Redistribution and use in source and binary forms, with or without 49 | modification, are permitted provided that the following conditions are met: 50 | * Redistributions of source code must retain the above copyright notice, this 51 | list of conditions and the following disclaimer. 52 | * Redistributions in binary form must reproduce the above copyright notice, 53 | this list of conditions and the following disclaimer in the documentation 54 | and/or other materials provided with the distribution. 55 | * Neither the name of the copyright holder nor the names of its 56 | contributors may be used to endorse or promote products derived from 57 | this software without specific prior written permission. 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 59 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 60 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 61 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 62 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 63 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 64 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 65 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 66 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 67 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 68 | */ -------------------------------------------------------------------------------- /force-app/main/default/classes/URL_CreateRecordController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/URL_CreateRecordControllerTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: Doug Ayers 3 | * Website: https://douglascayers.com 4 | * GitHub: https://github.com/douglascayers/sfdx-record-create-url-component 5 | * License: BSD 3-Clause License 6 | */ 7 | @IsTest 8 | private class URL_CreateRecordControllerTest { 9 | 10 | @IsTest 11 | private static void test_get_field_describe() { 12 | 13 | Test.startTest(); 14 | 15 | Map fieldDescribeMap = URL_CreateRecordController.getFieldDescribeMap( 'Account' ); 16 | 17 | System.debug( fieldDescribeMap ); 18 | 19 | System.assertEquals( true, ( (Map) fieldDescribeMap.get( 'Name' ) ).get( 'createable' ) ); 20 | System.assertEquals( false, ( (Map) fieldDescribeMap.get( 'Id' ) ).get( 'updateable' ) ); 21 | 22 | Test.stopTest(); 23 | 24 | } 25 | 26 | } 27 | /* 28 | BSD 3-Clause License 29 | Copyright (c) 2018, Doug Ayers, douglascayers.com 30 | All rights reserved. 31 | Redistribution and use in source and binary forms, with or without 32 | modification, are permitted provided that the following conditions are met: 33 | * Redistributions of source code must retain the above copyright notice, this 34 | list of conditions and the following disclaimer. 35 | * Redistributions in binary form must reproduce the above copyright notice, 36 | this list of conditions and the following disclaimer in the documentation 37 | and/or other materials provided with the distribution. 38 | * Neither the name of the copyright holder nor the names of its 39 | contributors may be used to endorse or promote products derived from 40 | this software without specific prior written permission. 41 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 42 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 43 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 45 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 46 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 47 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 48 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 49 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 50 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 51 | */ -------------------------------------------------------------------------------- /force-app/main/default/classes/URL_CreateRecordControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /images/cancel_save_button_behavior.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/sfdx-record-create-url-component/7f6de003af4683bc9338ea9da470508cf9b56f34/images/cancel_save_button_behavior.gif -------------------------------------------------------------------------------- /images/contact_url_hack_cmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/sfdx-record-create-url-component/7f6de003af4683bc9338ea9da470508cf9b56f34/images/contact_url_hack_cmp.png -------------------------------------------------------------------------------- /images/record-create-url-cmp-cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/douglascayers/sfdx-record-create-url-component/7f6de003af4683bc9338ea9da470508cf9b56f34/images/record-create-url-cmp-cancel.png -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "45.0" 11 | } 12 | --------------------------------------------------------------------------------