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.

  1. Screen action where clicking a button takes a user to a modal window on desktop or a tab view on mobile.

  2. 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

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.

MyScreenAction UI

Custom Action (headless)

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:

Quick Action Panel UI

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

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:

Cons:

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:

Unresolved questions

undefined