7

I am aware of the following options, but none of them quite do the job (and here's why):

Option What it can and cannot do
Aura component and the force:createRecord event Using this event I can open an overlay dialog, using a specified record type and various field values, allowing the user to create the new record using the appropriate page layout. However, this doesn't allow me to stay on the current page; the user is navigated to the new record's page instead after saving the new record.
LWC with embedded lightning-record-form I can write the wrapping LWC to present the lightning record form in an overlay dialog on top of the page and when the record has been created the dialog can close. I cannot, however, set the record type or any default values for fields.
LWC with embedded lightning-record-edit-form I can write the wrapping LWC to present the edit form in an overlay dialog on top of the page and when the record has been created the dialog can close. I can set the required record type and field values. I cannot, however, use the appropriate page layout and must explicitly list the fields to be presented and edited which doesn't fit with my use case (where I won't know the type of record nor the appropriate fields in advance).
LWC with Navigation Mixin to new record page I can have the LWC component open a separate browser tab/window to present a new record form, accepting a specified record type and various field values. While the user doesn't lose the current page, the UX flow is disrupted. The user needs to close the tab and find the one they started from. They may also need to explicitly refresh that originating tab to take the record creation into account.

I know I could write a component that leverages the uiRecordApi's getRecord to query the Create layout details, but this means a huge amount of custom coding just to plug the gap in the existing set of options that Salesforce provides. (The lightning base components package excludes all record-related base components since these only work on Salesforce and were not open sourced.)

I had a discussion with the Product Manager for LWC base components back in 2019 (Greg Rewis was the PM at that point) and tried to persuade him that the lightning-record-form should support explicit field initialization and record type selection, but I clearly failed (and he has since changed role).

Is anyone aware of any existing standard or open source way to achieve what I need to do, ideally 100% LWC?

6
  • For what it's worth, we went the route of reading the layout items to construct the record layout. It is, as you say, a lot of work to plug a seemingly small gap, but for what it's worth, it's required very little maintenance
    – Jeff Kranz
    Commented Sep 23, 2022 at 16:46
  • 1
    Thanks @JeffKranz... fancy open sourcing it?! ;)
    – Phil W
    Commented Sep 23, 2022 at 16:47
  • I would love to, getting my company involved in OSS is on my to-do list, but that won't be soon :(
    – Jeff Kranz
    Commented Sep 23, 2022 at 16:48
  • @JeffKranz ah well, it was worth a punt. Maybe someone else has a similar solution that is available. Fingers crossed...
    – Phil W
    Commented Sep 23, 2022 at 16:49
  • 1
    @ChristianSzandorKnapp, you're welcome. BTW, I'll be posting an answer covering a hacky way to get what I want, using an undocumented Salesforce feature (which is why it is hacky - it could stop working any time). Not quite ready for "prime time" yet. Keep your eyes peeled.
    – Phil W
    Commented Sep 27, 2022 at 12:55

1 Answer 1

4

After a fair amount of research and some development effort we managed to find a solution that:

  1. Allows us to open the standard Salesforce new record form as a modal dialog on top of our current page,
  2. Remain on our current page after creating the record and
  3. 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!

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .