7

Disclaimer: My answer to this question does not deal with a real business use case scenario. The approach mentioned in my answer was found to be working while going through this question. The answer can be definitely improved/fine-tuned based on real business use case scenarios.


Going through the LWC documentation or the lwc-recipe, there does not seem to be a scenario which discusses how to pass a SObject record from LWC to a custom Apex method to persist data. All the examples in documentation or lwc-recipe only discusses about fetching data.

The ones available are using the lightning/ui*APi* createRecord() or updateRecord(), but what if I need to pass it to custom Apex methods evaluating more complex logic before DML operations?

So, how do we send a SObject record to a custom Apex method for performing DML operations?

Let's say we have the following Use Case Scenarios:

  1. Create a new SObject record by constructing an instance of the SObject and passing values from LWC to a custom Apex method performing insert
  2. Update a SObject record fetched on LWC using @wire service by passing values from LWC to a custom Apex method performing updates
3
  • For scenario 2 - what would be the benefit over updating with updateRecord?
    – tsalb
    Apr 9 '19 at 2:47
  • @tsalb This was more from a learning perspective as how you can achieve this if the need be. I agree that you can use updateRecord.
    – Jayant Das
    Apr 9 '19 at 2:49
  • That's totally fair. I'll go ahead and post up an answer of the way I currently do it when I don't use lighting data service - but just a quick disclaimer this is not preferred!
    – tsalb
    Apr 9 '19 at 2:53
3

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;


    }

}
3
  • +1. This looks good.
    – Jayant Das
    Apr 9 '19 at 14:59
  • @PranayJaiswal, I don't understand why "[event.target.dataset.field]" is in square brackets (indicating this is an array?) when trying to set the required field value. Can you explain?
    – Phil W
    Dec 30 '19 at 10:11
  • 1
    @PhilW its to dynamically provide fieldName while creating an object. I asked similar question on SO. You can have a look : stackoverflow.com/a/55589799/4121395 Dec 30 '19 at 11:17
10

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;
  }
2
  • +1. Thanks for sharing this approach.
    – Jayant Das
    Apr 9 '19 at 15:00
  • 1
    Thanks for the help!
    – Atlas Can
    Apr 13 '19 at 18:33
4

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;
}
7
  • Not sure if getting response in string is a good idea Apr 9 '19 at 8:31
  • @PranayJaiswal This is not a real business use case but more from a learning perspective as how you can do so if you need it.
    – Jayant Das
    Apr 9 '19 at 10:54
  • Can you have a look at my Gist? I was able to create record without passing ` { 'sobjectType': 'Contact' };` gist.github.com/pranayjswl007/c5bec83bc2ff3b0c905edd9ecb3b34ae Apr 9 '19 at 11:13
  • @PranayJaiswal Good to know this. Seems the platform deserializes it based on the parameter type as in the apex method. What is the behavior if you say add a field in your JS which does not exist in Contact? Does that give you a compile time error?
    – Jayant Das
    Apr 9 '19 at 11:47
  • No compile time or run time error. It deserialized only the fields present on the contact. If you want string binding you can import feildName. gist.github.com/pranayjswl007/d0ef6305e8b4f0b084c48e74e5eb5a84 Apr 9 '19 at 12:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.