Status: ACCEPTED
Champion: Kamil Szostak
Revision: unknown
RFC created: 2020/05/01
Last updated: 2020/12/21
RFC: https://github.com/salesforce/lbc-api-rfcs/pull/9
This RFC is under review in PR (#16)
# Lightning Quick Actions API (lightning/actions)
# Summary
We are building an LWC version of Lightning quick actions and extending the functionality by providing options to create Screen and Headless actions.
-
Screen action where clicking a button takes a user to a modal window on desktop or a tab view on mobile.
-
Actions, aka Headless actions, do not route to an action layout and directly execute custom code; what the code does after that is entirely up to the action.
No custom logic is required to execute these actions, the framework will take care of building the URL and handling the navigation based on the action’s setup.
The purpose of this RFC is to create new APIs for developers to control the surrounding of LWC Custom quick actions. For instance: the panel window of a screen action or an action button state.
# Basic examples
# Custom Screen Action
-
my-screen-action-meta.xml
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>51.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__RecordAction</target> </targets> <targetConfigs> <targetConfig targets="lightning__RecordAction"> <actionType>ScreenAction</actionType> </targetConfig> </targetConfigs> </LightningComponentBundle>
-
my-screen-action.js
import { LightningElement, api } from 'lwc'; export default class MyScreenAction extends LightningElement { @api recordId; }
-
my-screen-action.html
<template> <p>Hello, LWC</p> <p>{recordId}</p> </template>
# Custom Screen Action UI
Screen action can be executed in different contexts. The template controls the content of the Screen, but not the execution context.
For instance, for modal window the template will be rendered inside the body
of the modal and the rest of the UI will be rendered and controlled by the framework.
# Custom Action (headless)
-
my-action-meta.xml
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>51.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__RecordAction</target> </targets> <targetConfigs> <targetConfig targets="lightning__RecordAction"> <actionType>Action</actionType> </targetConfig> </targetConfigs> </LightningComponentBundle>
-
my-action.js
import { LightningElement, api } from 'lwc'; export default class MyAction extends LightningElement { @api invoke() { console.log('Hello, world!'); } }
-
my-action.html
<template></template>
Headless action's template has to be empty.
# Motivation
Introduce the LWC version of Lightning quick actions and address limitations known from the Aura implementation of custom quick actions.
Make it easy and intuitive for developers to build LWC custom quick actions.
# Detailed design
# Common types
import { api, LightningElement } from 'lwc';
// Workaround for Promise type only supporting one parameter, taken from
// https://github.com/microsoft/TypeScript/issues/5413
interface PromiseWithError<T, U> extends Promise<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: U) => TResult2 | PromiseLike<TResult2>) | undefined | null
): PromiseWithError<TResult1 | TResult2, U>;
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(
onrejected?: ((reason: U) => TResult | PromiseLike<TResult>) | undefined | null
): PromiseWithError<T | TResult, U>;
}
# Lightning Web Components as actions
Screen and Headless actions are represented by Lightning Web Components. This will allow these actions to take the full advantage of all the features which are only available for LWCs, such as wires or custom events.
# Execution context
The action's execution context is defined in a metadata file by <targets>
.
Based on the target the framework will auto-populate specific properties in the custom component.
Initially, we will only support lightning__RecordAction
and the following properties will be auto-populated recordId
and objectApiName
(if present).
This behavior is consistent with the Record
and Object contexts.
In the future we will support other contexts: lightning__GlobalAction
, lightning__RelatedListAction
and lightning__MassAction
.
Each of them will come with their own set of auto-populated properties.
# Screen Action component and events
To implement a Screen Action a developer needs to create a Lightning Web Component by extending the LightningElement
class.
They can use the template to fully customize the UI of the action or dispatch a CloseActionScreenEvent
event to close the screen.
CloseActionScreen event
/**
* Closes a Quick Action screen
*
* When fired from within the `ScreenAction` it will close the screen (for instance, the panel window or tab view on mobile)
*/
declare class CloseActionScreenEvent extends CustomEvent<any> {
constructor(eventInt?: EventInit);
}
The CloseActionScreenEvent
event has a default event propagation settings (bubbles: false
and composed: false
).
This means that, using the default settings, only the action component can dispatch this event and close the modal.
If a developer wants to dispatch this event from a descendants components, they may want to override the event propagation settings by passing
an EventInit
object to the constructor.
Context-specific properties
A developer may add context-specific properties which will be automatically populated by the framework.
For instance, for a record context the framework will automatically populate recordId
and objectApiName
,
if those properties are present.
export class RecordAction extends LightningElement {
@api recordId?: string;
@api objectApiName?: string;
}
# <lightning-quick-action-panel>
component
Developers have full control over the body of the Screen action and they may fully customize it.
But if they want to keep a consistent UI across all actions (including standard actions defined by Salesforce and Aura Lightning quick actions),
they may want to use the <lightning-quick-action-panel>
component.
<template>
<lightning-quick-action-panel title="My action">
My custom component.
<div slot="footer">
<lightning-button variant="neutral" label="Cancel"></lightning-button>
<lightning-button variant="brand" label="Save"></lightning-button>
</div>
</lightning-quick-action-panel>
</template>
The component has a very similar UI to force:lightningQuickAction
and will provide a template for building a consistent UI for quick actions.
It has two slots: the default one for the body and footer for custom footer actions.
The header part of the UI will be rendered only if the title
property is a non-empty string.
Similar to the footer
slot, if the slot is not set, the footer part of the UI will no be shown.
If the developer wants to build a fully custom UI they should not use this component and instead put their markup directly in the action's template.
# <lightning-quick-action-panel>
component UI
When rendered the action will look like this:
# Alternative approach - the header slot
I considered adding a third slot for the header. With the current design the developers can only pass a string value and the header will always be rendered using the same template.
This enforces some level of consistency but limits the level of customization.
The header
slot allows full customization and requires a bit more code to create a default UI.
<slot slot="header">
<h1>My action</h1>
</slot>
# Action (headless) base class
To implement a headless action a developer needs to create a Lightning Web Component with an exposed invoke()
method.
# Example
import { LightningElement, api } from "lwc";
declare default class HeadlessSimple extends LightningElement {
@api invoke() {
console.log("Hi, I'm an action.");
}
}
# The invoke
method
The invoke
method will be called by the framework every time the action is triggered. For long-running
actions developers may want to add a check to prevent the action from being executed multiple times in
parallel. This can be done by adding an internal boolean flag which will work like a semaphore.
The framework considers the return type as void
. A developer may return a Promise
if they want to make their
method asynchronous, but the returned Promise
will be ignored.
import { LightningElement, api } from "lwc";
declare default class HeadlessAsync extends LightningElement {
isExecuting = false;
@api async invoke() {
if (this.isExecuting) {
return;
}
this.isExecuting = true;
await this.sleep(2000);
this.isExecuting = false;
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
# Drawbacks
- The usual cost of implementation and support of new code.
- This feature is not supported outside of LWC Custom quick actions and will not be back-ported to Aura quick actions.
- The feature cannot be implemented in user space. The custom code doesn't have access to the screen or the button to implement such functionality.
# Alternatives
We did consider an alternative design for Screen Actions where the developers would only control the body of the Action's UI, and the header and footer will be controlled using events. This would enforce a consistent look and feel across all custom actions and at the same time allow some customization.
/**
* Custom Screen footerer action.
*/
declare interface FooterAction {
label: string;
invoke: () => PromiseWithError<void, Error>;
primary: boolean;
}
/**
* Controls the title and footer actions of a Screen Quick Action.
*
* @param detail - screen details, a title and footer actions.
*/
declare class ConfigureScreenEvent extends CustomEvent<any> {
constructor(detail: { title?: string; footerActions?: Array<FooterAction> });
}
export default class DemoActionBasic extends LightningElement {
connectedCallback() {
const saveFooterAction = {
label: 'Save',
invoke: () => {
console.log('custom save logic');
},
primary: true,
};
const closeActionScreenFooterAction = {
label: 'Cancel',
invoke: () => {
this.dispatchEvent(new CloseActionScreenEvent());
},
primary: false,
};
this.dispatchEvent(
new ConfigureScreenEvent({
title: 'alternative action 1',
footerActions: [saveFooterAction, closeActionScreenFooterAction],
})
);
}
}
Pros and cons of the alternative approach:
Pros:
-
a consistent way to set screen titles and footer actions across all contexts, e.g. the event can be handled by the native mobile app or footer actions can be translated into mobile native buttons.
-
controlling the UI through events ensures the consistent look and feel
Cons:
-
not consistent with how other LWC components work. Developers are more familiar with
Slots
and they would need to learn new action specific events -
design cliff, easy to build a standard action without any customizations, but to customize footer or header developers need to build the whole UI from scratch
Considering that our main priorities are: allow full customizations and follow LWC design patterns, we've chosen the slot-driven approach.
# Adoption strategy
Those APIs will be shipped as part of the new LWC custom quick action feature. Aura quick actions (force:lightningQuickAction
) will not be affected.
# How we teach this
Those APIs will be shipped as part of the new LWC custom quick action feature. We will provide documentation in the Developer Portal, similar to the one which exists for the Aura Component - Lightning Quick Action
We are also considering:
-
a Trailhead project to teach how to create Screen and Headless actions
-
an SFDX plugin to create a new quick action from template
# Unresolved questions
- Inheritance. Compiler limitations on how far down it can go.