Passing Multiple Arguments to Remote Action using lightning-container NPM package

lightning-container

Does anyone know how to do this? I've tried just about everything, but can only successfully call an apex method that has a single parameter using LCC.callApex(...). The docs describe apexMethodParameters as "A JSON array of arguments for the Apex method", yet all examples I can find show apex methods with just a single parameter and the corresponding call to LCC.callApex being provided a string representing an atomic value, not an array.

For example, this works for calling an apex method with a single parameter of type string:

LightningContainer.callApex(
  "LightningContainerTest.getTenAccountsThatStartWith",
  "someString",
  handlerFunction,
  { escape: true }
);

However, to call a method with the following signature:

@RemoteAction
public static List<Account> getAccounts(string accountName, integer maxRecords) {...}

Nothing seems to work. Things I've tried:

LightningContainer.callApex(
  "LightningContainerTest.getTenAccountsThatStartWith",
  ["someString" 10],
  handlerFunction,
  { escape: true }
);
LightningContainer.callApex(
  "LightningContainerTest.getTenAccountsThatStartWith",
  { accountName: "someString", maxRecords: 10 },
  handlerFunction,
  { escape: true }
);

and even, ridiculously:

LightningContainer.callApex(
  "LightningContainerTest.getTenAccountsThatStartWith",
  [{ someString: "someString" }, {maxRecords: 10 }],
  handlerFunction,
  { escape: true }
);

While only some of these can be correctly described as JSON arrays, they are all more JSON-y than the working example from the docs (as a plain string isn't JSON or an array – unless you want to get technical and call it an array of chars). In any case, I'm just starting to think Salesforce didn't really think this one out very well and it simply doesn't work. The analogous JavaScript code for accomplishing the same thing in Visualforce shows something that won't work here, and I'm guessing the lightning-container npm package was cobbled together quickly, was not widely tested, was never widely adopted, and thus this just doesn't work and was never fixed.

My current solution is to just serialize an object into a string and deserialize it in the apex controller when I need multiple parameters, but would be grateful if anyone can share their experience of solving this differently.

Best Answer

Ok, so just after I posted this I went against the compiler and tried the same way it works in visual force and it actually does work. For the record, here's the built-in TypeScript signature for the callApex method:

export function callApex(fullyQualifiedApexMethodName: string,
                         apexMethodParameters: any,
                         callbackFunction: (result: any, event: any) => void,
                         apexCallConfiguration: any): void;

Which means, this will not work - the compiler simply won't allow it:

LightningContainer.callApex(
  "LightningContainerTest.getTenAccountsThatStartWith",
  "someString",
  10,
  handlerFunction, // error!
  { escape: true } // error! Expected 4 arguments, but got 5.
);

So, you have to opt-out of TypeScript, and JavaScript sensibilities in general for this to work:

LightningContainer.callApex(
  "LightningContainerTest.getTenAccountsThatStartWith",
  "someString",
  10,
  // @ts-ignore
  handlerFunction, 
  // @ts-ignore
  { escape: true }
);

Now the compiler doesn't complain, and the arguments get passed to the apex method in the order they're defined in this function call. A bit absurd. A little re-working of the underlying code could have supported something similar to Aura (just pass an object keyed by the apex method param names), or even an array, or better yet a rest param at the end of the function to accept any number or args without causing an issue and wondering how the heck they even handle this on the other end! Probably easier said than done, but if not for the lack of adequate documentation I would not be complaining at all :-)

Oh well, rant over.

Should anyone come across this, find an improved type signature below using variadic tuple types which will support this (in fact, I'm pretty sure one of the big reasons TypeScript introduced this construct was to adequately model legacy JavaScript code just like this):

export function callApex(fullyQualifiedMethodName: string, ...args: [
    // any number of args to apex method in the middle
    ...(string | number | boolean)[],
    // this callback function must be in the second to last position or it won't compile
    (result: any, event: any) => void,
    // this must be in the last position or it won't compile
    { escape: boolean }
]): void;

You can make these changes permanently directly in the npm package by using patch-package.


UPDATE:

I judge it to be pretty unlikely given how few people seem to be using this package (based on the fact that this bug is not fixed and how little info there is out there), but if anyone does come across this same issue, I should also say that the above solution only worked by chance for two arguments, and unless you patch more than the type definitions themselves, it doesn't seem like you could ever call an apex method with more than two arguments (even if you were using plain JS). Here is the patched version of the callApex method itself, as well as the correct type signature:

module.exports.callApex = function(fullyQualifiedApexMethodName,
                                   apexMethodParameters,
                                   callbackFunction,
                                   apexCallConfiguration) {
    if (typeof Visualforce !== "undefined" && 
        typeof Visualforce.remoting !== "undefined" &&
        typeof Visualforce.remoting.Manager !== "undefined") {
            Visualforce.remoting.Manager.invokeAction(fullyQualifiedApexMethodName,
                                                      ...apexMethodParameters,
                                                      callbackFunction,
                                                      apexCallConfiguration);
    }
    else {
        // TODO: offline
    }
}

compare this version to the original to see the bug :-)

And the new type signature:

export type ApexEvent<T> = {
  statusCode: number;
  type: "rpc" | "exception";
  tid: number;
  ref: boolean;
  action: string;
  method: string;
  status: boolean;
  result: T | null;
  message?: string;
  where?: string;
  data?: any;
}

export type ApexCallConfig = {
    escape: boolean;
}

export type ApexCallback<T> = (result: T, event: ApexEvent<T>) => void;

export function callApex<T = any>(fullyQualifiedApexMethodName: string, args: any[], callback: ApexCallback<T>, apexConfig: ApexCallConfig): void;