After a fair amount of research and some development effort we managed to find a solution that:
- Allows us to open the standard Salesforce new record form as a modal dialog on top of our current page,
- Remain on our current page after creating the record and
- Refresh our page when the form is closed (successfully or not).
Solution approach
The LWC implementation falls into two parts:
Opening the new record form without navigating
We found that there's an undocumented state parameter, navigationLocation
, that when set to RELATED_LIST
results in the current page being retained when the new record modal dialog is closed. In our case we wanted to ensure that only a single new record could be created at a time so we also set the count
state parameter.
We can use the normal payload for the navigation mixin to set the default field values, using the standard Salesforce page reference utils' encodeDefaultFieldValues
. We take these defaults from our event payload.
Observing the new record form being closed
This was quite tricky since there are no events to catch here and we needed to be able to refresh our LWC's content after the new record has been created.
The best we could do was to use a MutationObserver to watch for relevant changes in the DOM. We did this in a quite broad way to minimise the chance that this would stop working if Salesforce changes the markup around the dialog. The general pattern was to:
- Create the MutationObserver in connectedCallback() but do not connect it to the DOM (i.e. don't start listening at this point).
- In the event handler in the LWC, start listening to the DOM before opening the dialog. It listens for changes against the DOM's
body
as that is the safest node and does not depend on any LWC classes.
- Have a
_handleDOMMutation
handler that simply checks for when the records-modal-lwc-detail-panel-wrapper
object is added to the DOM and then updates the LWC when it is removed. This also disconnects the MutationObserver
again to avoid unnecessary overheads.
Solution implementation
Here are the relevant parts for this specific implementation:
import {NavigationMixin} from "lightning/navigation";
import {encodeDefaultFieldValues} from "lightning/pageReferenceUtils";
...
export default class MyLWC extends NavigationMixin(LightningElement) {
...
/**
* The MutationObserver used to watch for changes made in the DOM.
*
* @type {MutationObserver}
* @private
*/
_mutationObserver;
/**
* Whether the new record dialog is open or not. This is required to refresh the schedule after the new record
* dialog is closed.
*
* @type {boolean}
* @private
*/
_isNewRecordDialogOpen;
...
/**
* Add event listeners when the component is added to the DOM.
*/
connectedCallback() {
// Create a MutationObserver to monitor changes to the DOM, required to refresh the component after
// the new record creation dialog is closed.
let MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
this._mutationObserver = new MutationObserver(this._handleDOMMutation.bind(this));
}
}
/**
* Handles changes to the DOM tree as observed by the MutationObserver. This is
* required to refresh the component after the new record creation dialog is
* closed.
*
* @param {MutationRecord[]} mutations the DOM mutations. Must not be null.
* @private
*/
_handleDOMMutation(mutations) {
for (const mutation of mutations) {
// If nodes were added check to see if the new record dialog was added
// If the new record dialog was removed, refresh the schedule and
// disconnect the mutation observer
if (mutation.addedNodes.length !== 0 && !this._isNewRecordDialogOpen) {
this._isNewRecordDialogOpen =
(document.querySelector('records-modal-lwc-detail-panel-wrapper') !== null);
} else if (mutation.removedNodes.length !== 0 && this._isNewRecordDialogOpen
&& (document.querySelector('records-modal-lwc-detail-panel-wrapper') === null)) {
this._isNewRecordDialogOpen = false;
this._mutationObserver.disconnect();
// Refresh the component
this.refresh();
}
}
}
/**
* Event handler for creating a new record.
*
* The standard behaviour of the NavigationMixin redirects to the newly created
* record which will navigate away from the page with this component. To improve
* the user experience, an undocumented property
* "navigationLocation: 'RELATED_LIST"
* is used to prevent navigation to the new record. The MutationObserver is used
* to determine when the new record dialog is closed so that the component can
* be refreshed.
*
* @param {Event} event the event details. Must not be null.
*/
async handleCreateNewRecord(event) {
event.stopPropagation();
const defaultValues = event?.detail?.defaultValues;
// Remove the quoted strings from the default values as they will not be
// cast from string to the target data type when creating a new record
for (let [key, value] of Object.entries(defaultValues)) {
if (key !== 'RecordTypeId') {
defaultValues[key] = value.replaceAll(/(^'|'$)/g, '');
}
}
// Set the navigation payload and encode the default values. The state
// includes the undocumented property to keep the current page in play
// after the dialog is closed.
let navigationPayload = {
type: 'standard__objectPage',
attributes: {
objectApiName: event.detail.objectApiName,
actionName: 'new'
},
state: {
defaultFieldValues: encodeDefaultFieldValues(defaultValues),
count: '1',
nooverride: '1',
navigationLocation: 'RELATED_LIST'
}
};
// Set the record type if applicable
if (event.detail?.recordTypeId) {
navigationPayload.state['recordTypeId'] = event.detail?.recordTypeId;
}
// Start looking for the dialog being either added to or removed from
// the DOM so as to know when to refresh the component
const objToObserve = document.querySelector('body');
if (objToObserve && this._mutationObserver) {
this._mutationObserver.observe(objToObserve, {childList: true, subtree: true});
}
// Open the new record modal dialog
this[NavigationMixin.Navigate](navigationPayload);
}
Summary
This is a slightly brittle solution because it relies on both an undocumented behaviour, using the navigationLocation: 'RELATED_LIST'
state property in the navigation mixin, but also on looking for a specific element, records-modal-lwc-detail-panel-wrapper
, being added to or removed from the DOM to determine when to do the refresh.
This is, however, far less code to write compared with effectively recreating this standard Salesforce functionality, and bounded enough that we can update it if Salesforce makes changes to the markup used to render the modal dialog. Obviously, if they remove the undocumented property behaviour we are in some trouble, but let's hope that doesn't happen!