9

I have a lightning-record-edit-form with lightning-input-field components inside. I don't want to display the standard field label because the 40 character constraint is too much. Instead, I've created a custom label to give me room for all of the text and to allow me to translate it in a multi-lingual org. I've wrapped the field in the appropriate classes to make the field in-line with the rest of the fields in my form and give it the required asterisk (which doesn't appear if variant="label-hidden"). My code:

Component.js


import { LightningElement, api } from 'lwc';
// custom labels
import some_field_custom from '@salesforce/label/c.Adding_New_Company';

export default class MyLWC extends LightningElement {
    label = {
        some_field_custom,
    };
}

Component.html

<!-- inside lightning-record-edit-form -->

<div class="slds-form-element slds-form-element_stacked">
    <label for="" class="slds-form-element__label">
        <abbr title="required" class="slds-required">*</abbr>
        {label.some_field_custom}
    </label>
    <lightning-input-field variant="label-hidden"
        field-name="some_field__c" 
        required>
    </lightning-input-field>
</div>

An issue with this approach is that there isn't an association between the custom <label> and the <input> that the component generates. Is there any way of joining the field and the label together using the for attribute on the label?

Additionally, is it possible to hide the help text from the lightning-input-field when overriding the label?


Edit 2021-4-14 Some additional details. Putting the following into the LWC's HTML:

<label for="example">Example</label>
<lightning-input-field field-name="Test__c" id="example" variant="label-hidden"></lightning-input-field>

Renders as:

<label c-mycomponent_mycomponent="" for="example-172">Example</label>
<lightning-input-field c-mycomponent_mycomponent="" id="example-172" class="slds-form-element">
    <lightning-input class="slds-form-element" lightning-input_input-host="">
        <span lightning-input_input="" data-aria="" class="slds-assistive-text"></span>
        <div lightning-input_input="" class="slds-form-element__control slds-grow">
            <span lightning-input_input="" class="slds-checkbox">
                <input lightning-input_input="" type="checkbox" id="checkbox-232" name="Test__c" value="">
                <label lightning-input_input="" for="checkbox-232" class="slds-checkbox__label">
                    <span lightning-input_input="" class="slds-checkbox_faux"></span>
                    <span lightning-input_input="" class="slds-form-element__label slds-assistive-text">Test</span>
                </label>
            </span>
        </div>
    </lightning-input>
</lightning-input-field>

Screenshot:

The output field

While it visually looks correct, clicking on the label will not check the checkbox and a screen reader will still use the field label generated and not the supplied label.

Looking at the output, it seems that lightning-input-field will wrap its own lightning-input and the issue is that there's no means of accessing the generated label inside of the lightning-input as it will always default to what its parent component pulls for the field label.

4 Answers 4

4

(I'm providing a separate answer as this is a completely different solution approach).

When the provided components don't work, the alternative is building a new LWC - in this case, to replace lightning-input-field. This comes with complications: you can't simply let the component integrate with the lightning-record-edit-form; you need to handle getting the initial value from the record and setting the input state; you need to handle input events and store the values; you need to intercept the form's onsubmit handler and override the fields for which you've used a custom input component.

Here is a somewhat different approach I've been using lately for custom picklist implementations (e.g., show options from a custom MDT and store the value in a Text(255) field). It's not quite as streamlined as I'd like, but I think it works well. The trick is to pass a lightning-input-field into the new LWC in the default slot, and then the LWC can hide that input field and provide its own UI, handling the input field's data internally. The passed-in input field remains in the parent's DOM and will be seen and used by the lightning-record-edit-form.

Here's a working implementation, although it could probably be cleaned up. I used LDS Templates mostly as I found them; the help icon could probably be replaced with a lightning-button-icon. And I just hacked in the "tooltip" effect with :hover CSS, so there is no delay. Still, this should demonstrate the approach:

labeledCheckbox.html

<template>
  <div class="slds-form-element slds-form-element_stacked">
    <label for="checkbox" class="slds-form-element__label">
      <abbr if:true={required} title="required" class="slds-required">*</abbr>
      {label}
    </label>
    <div class="slds-form-element__icon">
      <button class="slds-button slds-button_icon" aria-describedby="help">
        <svg class="slds-button__icon" aria-hidden="true">
          <use xlink:href="/_slds/icons/utility-sprite/svg/symbols.svg#info"></use>
        </svg>
        <span class="slds-assistive-text">Help</span>
      </button>
      <div class="slds-popover slds-popover_tooltip slds-nubbin_bottom-left" role="tooltip" id="help" style="position:absolute;top:-60px;left:-16px;width:170px">
        <div class="slds-popover__body">{helptext}</div>
      </div>
    </div>
    <div class="slds-form-element__control">
      <input type="checkbox" id="checkbox" required={required} onchange={handleChange} checked={checked} >
    </div>
    <slot class="slds-hide" onslotchange={handleSlotChange}></slot>
  </div>
</template>

labeledCheckbox.js

import { LightningElement, api } from 'lwc';

export default class LabeledCheckbox extends LightningElement {
  @api label;
  @api helptext;
  inputField;

  get required() {
    return this.inputField?.required;
  }

  get checked() {  // set the UI to the initial field state
    return this.inputField?.checked;
  }

  handleSlotChange(event) {
    this.inputField = this.querySelector('lightning-input-field');
  }

  handleChange(event) {  // set the hidden field to the UI state
    if (this.inputField) this.inputField.value = event.target.checked;
  }

}

labeledCheckbox.css

.slds-popover_tooltip {
  display: none;
}

.slds-form-element__icon:hover .slds-popover_tooltip {
  display: inline-block;
}

And here's a form using the checkbox:

exampleForm.html

<lightning-record-edit-form object-api-name={objectApiName} record-id={recordId}>
  <div class="slds-col slds-grid slds-gutters_medium slds-grid_align-spread">
    <c-labeled-checkbox label={labels.nonProfit} helptext={help.nonProfit} class="slds-col slds-size_1-of-2">
      <lightning-input-field field-name={nonProfit}></lightning-input-field>
    </c-labeled-checkbox>    
  </div>
</lightning-record-edit-form>

exampleForm.js

import { LightningElement, api, wire } from 'lwc';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import ACCOUNT_NON_PROFIT_FIELD from '@salesforce/schema/Account.Non_Profit__c';

export default class ExampleForm extends LightningElement {
  @api objectApiName = ACCOUNT_OBJECT;
  recordId = '001000...'; // provide an example 
  nonProfit = ACCOUNT_NON_PROFIT_FIELD;

  @wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT }) objectInfoResponse;

  // Example only - you could load these values from labels
  get labels() {
    return {
      nonProfit: 'Non Profit 501(c)(3) Organization?'
    }
  }

  // as you add more fields that need help text passed in, add their class vars to fieldTags
  get help() {
    const help = {};
    const fieldDefs = this.objectInfoResponse?.data?.fields;
    if (fieldDefs) {
      const fieldTags = ['nonProfit'];
      fieldTags.forEach(tag => {
        const fieldName = this[tag].fieldApiName;
        help[tag] = fieldDefs[fieldName]?.inlineHelpText;
      });
    }
    return help;
  }
}

Sample Output:

Screenshot of example

This renders well, and clicking the label activates the checkbox as expected; the rendered html uses the same id for the label and the display input.

Rendered HTML

Notes, Caveats

This is designed to be fairly simple to use in the host form. Drop in the checkbox, and pass along a lightning-input-field set to the right field. You'll notice that we infer "requiredness" from the passed-in input field, but we cannot get the help text that way; lightning-input-field doesn't expose it. I would prefer if c-labeled-checkbox could look up the help text directly, the way that the host component does, but while you can get the field name from the passed-in input field, you cannot get the Object API Name from it.

One more note on required - when c-labeled-checkbox gets the value from the passed-in lightning-input-field, it will get the value from the field's definition, unless you override it explicitly on the passed-in input field, in which case it will get the overridden value. But for checkboxes, for some reason, they always report as required=true, unlike other input types. I left it as-is in order to demonstrate the method, but in this case, it will always be true. You could just make it an @api param of c-labeled-checkbox instead.

3
  • This is a wonderful answer! It's clear, clever, and has plenty of explanation. I'll be holding out hope Salesforce improves lightning-input-field but in the short-term this looks like a great alternative
    – nbrown
    Apr 16, 2021 at 12:55
  • Hi @JasonClark I love this idea and I am currently trying to implement this myself for a different field type. I am struggling with setting the 'slds-form-element slds-form-element_stacked' vs 'horizontal' class to respect what the record-edit-form is setting for all its direct standard lightning-input-field child components. Particularly when screen gets resized and horizontal gets forced to stacked. Do you have any ideas pls?
    – alesremta
    Nov 2, 2022 at 14:06
  • 1
    @alesremta Glad this helped you. I suggest you take a look at developer.salesforce.com/docs/component-library/documentation/… for a way to get width info passed in from the parent lightning page. You'd probably use this in your parent form and pass info as needed to your component. If that doesn't solve you issue, I recommend posting a new question about how to reactively change your layout. I'm no expert on CSS or LDS. Nov 2, 2022 at 18:18
3

Although it isn't documented, you can set an html id attribute on lightning-input-field. It will likely be changed (amended) by the system when it is rendered, however, a corresponding label for="id" will also be updated and will match the modified id value.

For example, I just tested the following in an lwc template:

<label for="example">Example</label>
<lightning-input-field field-name="Example__c" id="example" variant="label-hidden"></lightning-input-field>

which rendered on the page as:

<label for="example-483">Example</label>
<lightning-input-field id="example-483" class="slds-form-element">...
4
  • This is true, but I'm hesitant to accept this as an answer for two reasons. The association between the <label> and the <input> element isn't made (the for and id will be different) and this causes issues with accessibility and usability. For example, if the input type is a checkbox, clicking on the label does not check the box which is the usual expectation for an input of this type.
    – nbrown
    Apr 14, 2021 at 12:36
  • @nbrown In my testing, and in my example above, the system updated BOTH the for and the id values with the SAME modified value, such that they still match, and the association is maintained. I did not have a chance to test with a checkbox input; did you confirm that this does not work as intended? Apr 14, 2021 at 12:44
  • Yes I tested with a checkbox and it does not work as intended. I've updated my question with additional details, including the output HTML after the component renders. This seems to be a gap in configuration because the lightning-input-field component will always pass on the field label to its internal lightning-input. To make this work uniformly, it may just be necessary to use lightning-input components and handle the form submission with code rather than leveraging a lightning-record-edit-form.
    – nbrown
    Apr 14, 2021 at 13:03
  • 1
    Ah yes, now I understand the issue. I've posted a separate answer that should address your requirements. Apr 16, 2021 at 3:01
1

On making a click of the label also check the checkbox, this approach is working for me:

handleLabelClick(event) {
    // Click on the label toggles the matching input field
    const fieldName = event.target.dataset.fieldName;
    const selector = `lightning-input-field[data-field-name="${fieldName}"]`;
    const checkbox = this.template.querySelector(selector);
    checkbox.value = !checkbox.value;
}

with markup that has additional data- values added to relate the label div to the lightning-input-field:

<div class="slds-form-element">
    <div class="slds-form-element__control">
        <div class="checkbox">
            <lightning-input-field
                data-field-name={question.answerField}
                field-name={question.answerField}
                variant="label-hidden"
                onchange={handleInputChange}
            ></lightning-input-field>
            <label class="checkbox__label">
                <span class="checkbox__faux"></span>
            </label>
            <div
                class="form-control__label"
                onclick={handleLabelClick}
                data-field-name={question.answerField}
            >{question.name}</div>
        </div>
    </div>
</div>
0

I inspected the DOM and copied the label html code that Salesforce gives you with the native label when the variant is not hidden.

After hiding the label on the input field with the variant, I just added the native Salesforce label html from the DOM I copied right above my input field and it works like a charm.

It is also nice how it aligns the label with other inputs to the right or left that have the variant un-hidden.

<label lightning-groupedcombobox_groupedcombobox="" class="slds-form-element__label" data-main-label="" for="combobox-input-1666"><abbr lightning-groupedcombobox_groupedcombobox="" class="slds-required" title="required">*</abbr>Add your custom label here</label>
<lightning-input-field variant="label-hidden" field-name"yourFieldNameHere"></lightning-input-field>

You must log in to answer this question.

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