wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Mixing lightning-input-fields with custom data aware fields


Salesforce lightning offers a developer various ways to design custom forms when page layouts are not enough. The record-edit-form strikes a nice balance: it uses Lightning data service and allows one to design your own layout and field selection.

Beyond lightning-input-fields

Most of the time lightning-input-field is all you need for this forms. They auto-magically talk to the UI API and display the right input type.

However there are cases, where that's not what your users want. A recent example from a project: Phone numbers are stored as text field in Salesforce, but the users wanted a guided input: a country picker, then showing the area code picker (if the country has those) and an checker for field length for the main number (which varies greatly by country) and an eventual extension field (popular in the US, but not elsewhere).

So I started digging. Shouldn't it be possible to have something like <c-phone-helper field-name="Phone" /> and the same data magic as for lightning-input-field would happen? Turns out: not so fast. With events and a little code it would be possible, but that glue code needed to be applied to any custom field.

This got me thinking. The solution, it turns out, was to "extend" the record-edit-form to handle "rough" input components. You can give the result a try in experiment 8

Design goals

  • The component should be a drop-in "replacement" for record-edit-form
  • Structure of a page should be similar to they way one builds record-edit-form based forms
  • All lightning-input-fields should work out of the box
  • No additional glue code should be required in the component hosting the new form
  • Custom input field types should be easy to build. Once I figure out extensions, based on a base component
  • Opinionated: form layout is using a lightning-layout

Results

The replacement for lightning-record-form is c-extended-form (from experiment 8).
"Replacement" is a mouth-full, since the component just wraps around a lightning-record-form. A few components are ready to be used for it:

  • specialInput a little test component. It just returns the input in upper case. Not very useful other than studying the boiler plate
  • uxDebouncedInput returns changed values after a debounce period. Default is 300ms, the attribute delay allows to specify duration. The component shows different behavior depending on the attribute field-name being present with a value. The original purpose of the field is to be used in uxQuickLookup, now you can use it standalone
  • uxQickLookup which allows you to lookup an object. It works in lightning apps, mobile and communities and can serve as a stop-gap for the missing lookup on mobile. I recently updated it to show additional fields beside the object name

How it works

A sample form looks like this:

<template>
    <lightning-card title="Form Sample">
        <div class="slds-p-horizontal_small">
            <c-extended-form object-api-name="Account">
                <lightning-layout-item size="6" padding="around-medium">
                    <lightning-input-field
                        field-name="Name"
                    ></lightning-input-field>
                </lightning-layout-item>
                <lightning-layout-item size="6" padding="around-medium"
                    ><lightning-input-field
                        field-name="AccountSource"
                    ></lightning-input-field
                ></lightning-layout-item>
                <lightning-layout-item size="6" padding="around-medium"
                    ><lightning-input-field
                        field-name="AccountNumber"
                    ></lightning-input-field
                ></lightning-layout-item>
                <lightning-layout-item size="6" padding="around-medium"
                    ><c-special-input
                        field-name="Phone"
                        data-field
                    ></c-special-input>
                </lightning-layout-item>
            </c-extended-form>
        </div>
    </lightning-card>
</template>

The extended form supports the same properties as lightning-input-form and follows the same structure. You might miss the submit button and the lightning-messages elements. They are already contained inside the extended form, as it the lightning-layout.

The above sample shows the setup for new Account objects, since the record-id attribute is missing. The form itself is quite light, the magic is in the handler functions:

<template>
    <lightning-record-edit-form
        record-type-id={recordTypeId}
        record-id={recordId}
        object-api-name={objectApiName}
        onload={formLoadHandler}
        onsubmit={formSubmitHandler}
        onsuccess={formSuccessHandler}
    >
        <lightning-layout multiple-rows="true">
            <lightning-layout-item size="12">
                <lightning-messages> </lightning-messages>
            </lightning-layout-item>
            <slot></slot>
            <lightning-button
                class="slds-m-top_small"
                variant="brand"
                type="submit"
                name="save"
                label="Save"
            >
            </lightning-button>
        </lightning-layout>
    </lightning-record-edit-form>
</template>

There are just 6 elements in the form:

Building custom input fields

When you want to build a custom input field, there are only a few requirements

  • Have a @api fieldName property for the field name you want to update
  • Have a @api value property for receiving the current value
  • Fire a Custom Event with the name valueChanged and a detail object that is name and value: {name: this.fieldName, value: this.value}
  • Add it to the form with an attribute data-field. This was necessary, so we can find them using a querySelector. I could have used a specific class name, but that's harder to understand when looking at it later (what's the class for? Is it style or selection - bad for maintenance). Initially I thought to use extended-field-name as attribute and use that in the querySelector, but that didn't work. Anyway having a consistent name for the field name attribute lowers your error rate.

So when adding input components (structure omitted for readability) you would see something like this:

<c-special-input data-field field-name="Phone"></c-special-input>
<c-ux-debounced-input data-field field-name="FirstName"></c-ux-debounced-input>
<c-ux-quick-lookup data-field field-name="Account"
                   object-api-name="Account"
                   label="Pick an account"
                   fields="Industry,Type,AccountNumber"></c-ux-quick-lookup>

Loading Data

This is handled in the formLoadHandler. Lightning data service provides all information we might need. In this component we pull the field values. Those values are either the current value of the field, for an existing record or the default for a new record.

Thereafter we grab all fields with the data-field attribute and provide a value if we have one in the record for that field-name (called as node.fieldName). It takes a while to get used to the permanent switch between kebab-case and CamelCase.

    formLoadHandler(event) {
        let fields = event.detail.record.fields;
        const defaultValues = {};
        for (let f in fields) {
            if (fields.hasOwnProperty(f)) {
                let elem = fields[f];
                let val = elem.displayValue || elem.value;
                if (val) {
                    defaultValues[f] = val;
                }
            }
        }
        // Now the input fields
        let specialNodes = this.querySelectorAll('[data-field]');
        for (let i = 0; i < specialNodes.length; i++) {
            let specialNode = specialNodes[i];
            let fieldName = specialNode.fieldName;
            if (defaultValues[fieldName]) {
                specialNode.value = defaultValues[fieldName];
            }
        }
    }

Handling Changes

Handling changes has 2 components. First we need to attach a event listener watching out for the valueChanged event. This is done in the constructor. Since events bubble, we don't need to bind one to each of our special input fields.

    constructor() {
        super();
        // Holds our fields
        this.specialFieldMap = {};
        this.addEventListener('valueChanged', this.listenDataChange, true);
    }

One of the little stumbling blocks for the listenDataChange was that in a normal event handler function the this keyword binds to the event target. So this.specialFieldMap in the component would be unaccessible. The solution here is to use an ES6 arrow function that doesn't rebind this. Once you make friends with ES6, there is no way back.

 /* Capturing all changed values from our data control */
    listenDataChange = event => {
        let fieldInfo = event.detail;
        this.specialFieldMap[fieldInfo.name] = fieldInfo.value;
    };

The syntax is something to get used to, especially when you have function that don't take input parameters and you see let x = () => { return Math.random()} - which just returns a random when you call x()

Saving back

The final piece is saving data back to Salesforce. The onsubmit event provides access to the whole form and we can easily mix in the values we have collected from the fields' valueChanged event. The beauty: no special server code required. We operate in the context of the lightning data service.

formSubmitHandler = event => {
        event.preventDefault();
        const fields = event.detail.fields;
        for (let key in this.specialFieldMap) {
            if (this.specialFieldMap.hasOwnProperty(key)) {
                fields[key] = this.specialFieldMap[key];
            }
        }
        this.template
            .querySelector('lightning-record-edit-form')
            .submit(fields);
    };

For good measure we indicate success and redirect to the record form:

   formSuccessHandler(event) {
        const record = event.detail;
        const evt = new ShowToastEvent({
            title: 'Record created',
            message:
                this.objectApiName +
                ' created: ' +
                (record.fields.Name.displayValue || record.fields.Name.value),
            variant: 'success'
        });
        this.dispatchEvent(evt);
        // Now navigate
        let destination = {
            type: 'standard__recordPage',
            attributes: {
                recordId: record.id,
                objectApiName: this.objectApiName,
                actionName: 'view'
            }
        };
        this[NavigationMixin.Navigate](destination);
    }

Go check it out and don't forget: YMMY!


Posted by on 06 April 2019 | Comments (0) | categories: Lightning Salesforce WebComponents

Comments

  1. No comments yet, be the first to comment