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 operator | showsWhen 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;}