How do I pass a SObject record from LWC to a custom Apex method to persist data?
In LWC, it's now preferable to attempt to use Lightning Data Service first, which includes any of the ui*Api adapters such as getRecord, createRecord and updateRecord:
The simplest way to work with data is to use the base Lightning components built on LDS: lightning-record-form, lightning-record-edit-form, or lightning-record-view-form components.
If you need more customization than those components allow, use @wire to specify a Lightning Data Service wire adapter. Each wire adapter gets a different data shape.
Let's say you've exhausted your options and are looking for a scalable, repeatable way to consistently pass data between client and server. An extremely flexible datatype is then Map<String, Object>
:
lwcTest.html
<lightning-button label="Mutate Data" onclick={mutateData}></lightning-button>
<lightning-button label="Update Complex" onclick={updateComplexData}></lightning-button>
<lightning-button label="Update Account" onclick={updateAccountData}></lightning-button>
lwcTest.js
import { LightningElement, wire, api } from 'lwc';
import wireSimpleOrComplexData from '@salesforce/apex/DataServiceCtrl.wireSimpleOrComplexData';
import updateComplex from '@salesforce/apex/DataServiceCtrl.updateComplex';
import updateAccount from '@salesforce/apex/DataServiceCtrl.updateAccount';
export default class LwcTest extends LightningElement {
@api
get recordId() {
return this._accountId;
}
set recordId(value) {
this._accountId = value;
}
// private
_accountId;
_wiredData;
_account;
_contacts;
_opportunities;
@wire(wireSimpleOrComplexData, { accountId: '$_accountId' })
wiredData(result) {
this._wiredData = result; // to preserve refreshApex if needed
if (result.data) {
// for single sobject object spread works since this is a shallow clone
this._account = { ...result.data.accountFromServer };
// for collections, since every element in array is proxied, we need a deep clone
this._contacts = JSON.parse(JSON.stringify(result.data.contactsFromServer));
// if complex objects are wanted, it might be better to do this at the result.data level
this._opportunities = JSON.parse(JSON.stringify(result.data.opportunitiesFromServer));
console.log(result.data.user.firstName); // UserInfo.getFirstName()
console.log(result.data.system.now); // System.now()
} else {
console.log(result.error);
}
}
// Before reaching this, all the following data had their read only proxies removed
mutateData() {
this._account.Name += ' Updated';
for (let contact of this._contacts) {
contact.Email = contact.LastName + '@test.com';
}
for (let opp of this._opportunities) {
opp.Name += ' Updated';
}
}
updateComplexData() {
const dataToUpdate = {
accountAfterMutate: this._account,
contactsAfterMutate: this._contacts,
opportunitiesAfterMutate: this._opportunities
}
updateComplex({data: dataToUpdate})
.then(result => {
// on success, you can bind to a tracked vars to re-render them
console.log(result);
})
.catch(error => {
console.log(error);
});
}
updateAccountData() {
updateAccount({accountToUpdate: this._account})
.then(result => {
// on success, you can bind to a tracked account variable for template view to re-render the template
console.log(result);
})
.catch(error => {
console.log(error);
});
}
}
DataServiceCtrl.cls
@AuraEnabled (cacheable=true)
public static Map<String, Object> wireSimpleOrComplexData(String accountId) {
Map<String, Object> result = new Map<String, Object>();
// Data 1 single sobject
Account acc = [SELECT Id, Name FROM Account WHERE Id =: accountId LIMIT 1];
result.put('accountFromServer', acc); // otherwise will be an array of 1 if directly SOQL-ed
// Data 2 collections
result.put('contactsFromServer', [SELECT Id, LastName FROM Contact WHERE AccountId =: accountId]);
result.put('opportunitiesFromServer', [SELECT Id, Name FROM Opportunity WHERE AccountId =: accountId]);
// Data 3 nested properties like a POJO
Map<String, String> userInfoMap = new Map<String, String>();
userInfoMap.put('firstName', UserInfo.getFirstName());
Map<String, Object> systemInfoMap = new Map<String, Object>();
systemInfoMap.put('now', System.now());
result.put('user', userInfoMap);
result.put('system', systemInfoMap);
return result;
}
@AuraEnabled
public static Map<String, Object> updateComplex(Map<String, Object> data) {
// Because sobjects were directly used, we can use this serialize/deserialize trick to get it back into a useable state
Account account = (Account) JSON.deserialize(
JSON.serialize(data.get('accountAfterMutate')),
Account.class
);
List<Contact> contacts = (List<Contact>) JSON.deserialize(
JSON.serialize(data.get('contactsAfterMutate')),
List<Contact>.class
);
List<Opportunity> opportunities = (List<Opportunity>) JSON.deserialize(
JSON.serialize(data.get('opportunitiesAfterMutate')),
List<Opportunity>.class
);
// you could put database.saveResult in here if you want
Map<String, Object> updateResults = new Map<String, Object>();
update account;
update contacts;
update opportunities;
updateResults.put('account', account);
updateResults.put('contacts', contacts);
updateResults.put('opportunities', opportunities);
return updateResults;
}
@AuraEnabled
public static Account updateAccount(Account accountToUpdate) {
// no need to serialize/deserialize or transport as JSON here
update accountToUpdate;
return accountToUpdate;
}
I started looking around and took a cue from the topic Migrate Apex, which states:
Aura components and Lightning web components both use an Apex controller to read or persist Salesforce data. There are no syntax differences for the two programming models.
So based on how it worked in Lightning Aura Components, I attempted to see if it worked in LWC as well, and Yes, it does work.
In summary, I needed to make sure that I represent the SObject
record/data as a JSON and then pass that as a parameter.
Below is the sample code that worked for me, where I was able to construct a JSON/manipulate an existing JSON and then pass it as a parameter to a custom Apex method to create/update a record.
HTML
<lightning-card title="My Hello World" icon-name="standard:contact">
{recordId}
</lightning-card>
<lightning-button label="Create Record" onclick={createRecord}></lightning-button>
<lightning-button label="Update Record" onclick={udpateRecord}></lightning-button>
JavaScript
import createContactRecord from '@salesforce/apex/ContactController.createContactRecord';
import updateContactRecord from '@salesforce/apex/ContactController.updateContactRecord';
import myContact from "@salesforce/apex/ContactController.fetchContact";
....
@track recordId;
contactRecord;
// fetches a contact record from Apex
@wire (myContact)
fetchedContact({error, data}){
if(data){
// this is where I save the fetched contact which will be updated later
this.contactRecord = JSON.stringify(data);
...
...
}
...
}
// my create record JS function, where I construct a SObject and am able to
// successfully create a record
createRecord() {
// created a JSON representation of the Contact record,
// same as we would do in Lightning Aura Components
let cont = { 'sobjectType': 'Contact' };
cont.FirstName = 'Jayant';
cont.LastName = 'From LWC';
createContactRecord({newRecord: cont})
.then(result => {
this.recordId = result;
console.log(result);
})
.catch(error => {
console.log(error);
this.error = error;
});
}
// my update record JS function, where I manipulate the JSON
// and set some values to be able to successfully update the record
updateRecord() {
let cont = JSON.parse(this.contactRecord);
// update the fields those are required to be updated
cont.LastName = '-LWC1';
updateContactRecord({recordForUpdate: cont})
.then(result => {
this.wiredContact = result;
console.log(result);
})
.catch(error => {
console.log(error);
this.error = error;
});
}
Apex
@AuraEnabled(cacheable=true)
public static Contact fetchContact(){
return [SELECT Id,Name, LastName FROM Contact where Id='xxxx' LIMIT 1];
}
@AuraEnabled
public static String createContactRecord(Contact newRecord){
insert newRecord;
return newRecord.Id;
}
@AuraEnabled
public static String updateContactRecord(Contact recordForUpdate){
update recordForUpdate;
return recordForUpdate.Name;
}
Here is my code which I use to CRU records in LWC. Fairly basic example. I am not using String or JSON manipulation. I am also using static binding using fieldName imports
HTML:
<lightning-input label="FirstName" value={realFormData.FirstName} if:true={realFormData} onchange={updateValue} data-field="FirstName"></lightning-input>
<lightning-input label="LastName" value={realFormData.LastName} if:true={realFormData} onchange={updateValue} data-field="LastName"></lightning-input>
{recordId} <br/>
<button class="slds-button" onclick={saveRecord}> Save Record</button>
<br/>
<button class="slds-button" onclick={createRecord}> Create new hardcored CONTACT Record and load in UI</button>
`
JS:
import { LightningElement ,wire,track,api } from 'lwc';
import getMyContact from "@salesforce/apex/ContactController.fetchContact";
import updateMyContact from "@salesforce/apex/ContactController.updateContact";
import createMyContact from "@salesforce/apex/ContactController.createContact";
import { refreshApex } from '@salesforce/apex';
import CONTACT_FIRSTNAME from '@salesforce/schema/Contact.FirstName';
import CONTACT_LASTNAME from '@salesforce/schema/Contact.LastName';
export default class MyCmp extends LightningElement {
@api wiredContact;
@api recordId;
@api realFormData;
@wire (getMyContact , { contactId: '$recordId' })
fetchedContact( resp){
this.wiredContact = resp;
this.realFormData = {... this.wiredContact.data};
}
updateValue(event){
this.realFormData = {...this.realFormData , [event.target.dataset.field] : event.detail.value};
console.log( this.realFormData);
}
saveRecord(event ){
updateMyContact({con : this.realFormData}).then(()=>{
console.log('Refresh Apex called');
refreshApex(this.wiredContact);
});
}
createRecord(event ){
let newContact = { [CONTACT_FIRSTNAME.fieldApiName] : 'Pikachdu' ,[CONTACT_LASTNAME.fieldApiName] : 'Raichu' };
createMyContact({con : newContact}).then((resp)=>{
this.recordId = resp.Id; //this will auto call wireMethod/
}).catch((err) => {
// Handle any error that occurred in any of the previous
// promises in the chain.
console.log(JSON.stringify(err));
});
}
}
Apex:
public class ContactController {
@AuraEnabled(cacheable=true)
public static Contact fetchContact(Id contactId){
return [SELECT Id,FirstName,LastName FROM COntact where id=:contactId LIMIT 1];
}
@AuraEnabled
public static void updateContact(Contact con){
update con;
}
@AuraEnabled
public static contact createContact(Contact con){
insert con;
return con;
}
}