Skip to content

Build a custom form

This article will focus on the creation of a custom dynamic form using useForm, BoltElement and <c-bolt-form>.

The form will create a Case record of a given type based on the choice made on a lightning-combobox>.

Barebone

Let’s start with this barebone.

<template>
<lightning-combobox
label="Case type"
options={caseTypes}
value={caseType}
data-bind="caseType"
onchange={bind}
></lightning-combobox>
</template>
import {
BoltElement,
mix,
useDataBinding
} from 'c/boltage';
export default class myLWC extends mix(
BoltElement,
useDataBinding
) {
@track caseType;
get caseTypes() {
return ['IT_SUPPORT', 'BILLING','GENERAL']
.map(type => ({
label: type,
value: type
}));
}
}

Making it dynamic

Let’s implement useForm and make use of the <c-bolt-form> component right away.

<template>
<lightning-combobox
label="Case type"
options={caseTypes}
value={caseType}
data-bind="caseType"
onchange={bind}
></lightning-combobox>
<c-bolt-form record={$Case} ></c-bolt-form>
</template>
import {
BoltElement,
mix,
useDataBinding,
useForm
} from 'c/boltage';
export default class myLWC extends mix(
BoltElement,
useDataBinding,
useForm({
fields: self => self.caseFields,
objectApiName: 'Case'
})
) {
@track caseType;
get caseTypes() {
return ['IT_SUPPORT', 'BILLING','GENERAL']
.map(type => ({
label: type,
value: type
}));
}
}

Here on line 9 of the html you can see that we’re passing down to <c-bolt-form> the property $Case injected on the this context by useForm.

Altough, at this stage, nothing will work yet, simply because we’re missing something very useful : the fields.

Let’s say the fields the user will need to fill in, differ from one case type to another, that’s why we’ve specified a function for the fields parameter on line 11. This function is returning what appears to be a getter called caseFields, let’s create it !

import {
BoltElement,
mix,
useDataBinding,
useForm
} from 'c/boltage';
import {
IT_SUPPORT_FIELDS, BILLING_FIELDS, GENERAL_FIELDS
} from './formFields.js';
export default class myLWC extends mix(
BoltElement,
useDataBinding,
useForm({
fields: self => self.caseFields,
objectApiName: 'Case'
})
) {
@track caseType;
get caseTypes() {
return ['IT_SUPPORT', 'BILLING','GENERAL']
.map(type => ({
label: type,
value: type
}));
}
get caseFields() {
switch(this.caseType) {
case 'IT_SUPPORT': return IT_SUPPORT_FIELDS;
case 'BILLING': return BILLING_FIELDS;
case 'GENERAL': return GENERAL_FIELDS;
}
}
}

Now that caseFields is created, @track caseType updates whenever the <lightning-combobox>’s selected item changes. This automatically fetches a new set of fields via useForm, making them available on the $Case prop. As $Case is passed to <c-bolt-form>, a new form renders each time.

Dependant form fields

Now, imagine our generated form fields have dependencies. For example, the input tied to Contact.maidenName__c should only become visible and editable if the Contact.gender__c input was previously set to ‘Female’.

Convenientely, <c-bolt-input> rendered by <c-bolt-form> exposes two APIs called controls and shows-when to achieve just that.

<c-bolt-input
lwc:spread={$Contact.gender__c}
controls={controlledFields}
></c-bolt-input>
<c-bolt-input
lwc:spread={$Contact.maindenName__c}
shows-when={condition}
></c-bolt-input>
export default class myLWC extends LightningElement {
controlledFields = ['maindenName__c'];
condition = {
gender__c: 'female'
};
}

One question might rise up in your mind though : how could I access these generated <c-bolt-input> components, make use of these APIs ?

That’s exaclty the moment to leverage another API exposed by useForm called withDefaultValues().

import { ... } from 'c/boltage';
import { ... } from './formFields.js';
export default class myLWC extends mix( ... ) {
@track caseType;
get caseTypes() { ... }
get caseFields() { ... }
get $CaseWithDefaultValues() {
return this.withDefaultValues(this.$Case, {
// anything
})
}
}
<template>
<lightning-combobox
label="Case type"
options={caseTypes}
value={caseType}
data-bind="caseType"
onchange={bind}
></lightning-combobox>
<c-bolt-form record={$Case} ></c-bolt-form>
<c-bolt-form record={$CaseWithDefaultValues} ></c-bolt-form>
</template>

You can see that we replaced the record attribute passed down to <c-bolt-form> with the new getter $CaseWithDefaultValues that returns a modified version of the base $Case.

That modified version can have whatever key you want. Each keys of that object will be bound passed down to <c-bolt-input> as attributes. So that’s the best place to implement our dependant field logic by using controls and shows-when (in JS land, we need to change that to camelCase => showsWhen).

import { ... } from 'c/boltage';
import { ... } from './formFields.js';
export default class myLWC extends mix( ... ) {
@track caseType;
get caseTypes() { ... }
get caseFields() { ... }
get $CaseWithDefaultValues() {
return this.withDefaultValues(this.$Case, {
brokenPcPart__c: {
controls: ['pcPartName__c']
},
pcPartName__c: {
showsWhen: {
brokenPcPart__c: true
}
}
})
}
}

Fields can be dependant on each other on multiple levels and the showsWhen condition syntax, supports values of type

  • boolean
  • string
  • number - with support for <, <=, >, >= operators.
Math operatorshowsWhen syntax
>gt
<lt
>=geq
>=leq
import { ... } from 'c/boltage';
import { ... } from './formFields.js';
export default class myLWC extends mix( ... ) {
@track caseType;
get caseTypes() { ... }
get caseFields() { ... }
get $CaseWithDefaultValues() {
return this.withDefaultValues(this.$Case, {
estimatedLoss__c: {
controls: ['brokenPcPart__c']
},
brokenPcPart__c: {
showsWhen: {
estimatedLoss__c: {
gt: 500
}
},
controls: ['pcPartName__c']
},
pcPartName__c: {
showsWhen: {
brokenPcPart__c: true
}
}
})
}
}

Field value change’s side effects

By default, rendered <c-bolt-input> are bound to their respective JS properties and available as a big object, either under the name Case__ref or Case depending on the mode insert or edit.

But, if you want to apply some side effects for one specific input change, you can do so by using the watch attribute of <c-bolt-form>. It receives an array of fieldApiName and each time on of these specified field would change, a custom event of that field name will emit, and thus, can be listened on <c-bolt-form> to apply your side effects.

import { ... } from 'c/boltage';
import { ... } from './formFields.js';
export default class myLWC extends mix( ... ) {
watch = ['Contract'];
handleContractUpdate(e) {
sideEffect();
this.next(e);
}
@track caseType;
get caseTypes() { ... }
get caseFields() { ... }
get $CaseWithDefaultValues() {
return this.withDefaultValues(this.$Case, { ... }
}
}
<template>
<lightning-combobox ... ></lightning-combobox>
<c-bolt-form
watch={watch}
oncontract={handleContractUpdate}
record={$CaseWithDefaultValues}
></c-bolt-form>
</template>

Notice on line 7 the use of this.next(e). It ensures the continuity of the value binding process.

Advanced DML behaviour

By default you’ll have to manually save the record input by using useDML’s this.saveRecord() method for instance. But, you can configure useForm to trigger a DML statement each time an input value changes by setting the parameter saveOnChange to true. You can apply some side effects that will run after the DML statement by implementing the autoSavedCallback() method.

import {
BoltElement,
mix,
useDataBinding,
useForm
} from 'c/boltage';
export default class myLWC extends mix(
BoltElement,
useDataBinding,
useForm({
fields: self => self.caseFields,
objectApiName: 'Case',
saveOnChange: true
})
) {
autoSavedCallback(e) {
doSomething();
}
}

In addition to front end input validation error messages, you can get and display the error messages coming from validation rules defined on the back end. To do that you must set the parameter asyncErrors to true in the useForm call.

import {
BoltElement,
mix,
useDataBinding,
useForm
} from 'c/boltage';
export default class myLWC extends mix(
BoltElement,
useDataBinding,
useForm({
fields: self => self.caseFields,
objectApiName: 'Case',
saveOnChange: true,
asyncErrors: true
})
) {
...
}
<template>
<c-bolt-form
lwc:ref="form"
record={$Case}
></c-bolt-form>
</template>

Styling 💅

To add custom styles to all those inputs you can either

  • use useStyles
  • add your own <style></style> tag in your community’s <head> markup.

Regardless, to actually put some styles specific to one input, you can make use of the attribute selector that should follow this pattern

[field="<ObjectApiName>.<FieldApiName>"] {
/** all your css declarations for that specific input **/
}

For instance, following the previous example, we would have something like :

[field="Case.brokenPcPart__c"] {
border: red;
border-radius: 100vh;
--some-lwc-custom-props: foobar;
}
[field="Case.pcPartName__c"] {
border: blue;
border-radius: 0;
}