List View API Updates in Spring’25 Release

Intro

Did you hear about the new Spring’25 feature that allows you to sort List Views by multiple columns?! Amazing right? The idea that was requested in 2007, was finally delivered after 17 years…

Even though this is a really nice feature, this post will not be about it, but still will be related to List Views.
So, one more time.

Did you hear about the new Spring’25 update in lightning/uiListsAPI? (Do not confuse it with lightning/uiListApi that is being deprecated; the new one has prular Lists 😉).

If not, then you are in the right place!
And if yes, then you are also in the right place! 😄
Let’s dive into it!

What’s new?

This new wire adapter allows to operate on List Views to perform full CRUD (Create, Read, Update, Delete) operations, which was not possible before. Previously, we could only read data from List Views.

The important note here is that even in Apex, the option to query List View details is limited to just the name, SObjectType, and other basic information, without the details about the filters, columns, sorting, and visibility.

What functions are available in the adapter?

The new lightning/uiListsApi adapter in
force is here to make your life easier. Here’s what it can do for you:

  • createListInfo: This function lets you create a new List View. All you need is the object name and the list view name. You can also specify filter logic, columns, sorting, and visibility.
  • updateListInfoByName: This function lets you update an existing list view. You need to provide the object name and the list view name. You can change filter logic, columns, sorting, and visibility.
  • deleteListInfo: This function deletes a List View. You just need to provide the object name and the list view name.
  • getListInfoByName: This function fetches the details of a list view. You need to provide the object name and the list view name.
  • getListInfosByName: This function fetches the details of multiple list views. You need to provide the names of the list views in an array.
  • getListInfosByObjectName: This function fetches the details of all list views associated with a specific object. You just need to provide the object name.
  • getListObjectInfo: This function provides details about the list view object.
  • getListRecordsByName: This function fetches the records from a list view based on specified conditions like filters and sorting.
  • getListPreferences: This function fetches the user’s preferences for a list view, such as column width, column order, and column wrap.
  • updateListPreferences: This function allows you to update a user’s preferences for a list view, including column width, column order, and column wrap.

But is it only for LWC?

No! All the adapters for LWC are in reality just wrappers for some of the functionalities (including API adapters). In this case, all of these actions are available in UI API, which means it can be used by any other technology that can authorize to Salesforce, including Apex, and other technologies.

Here is the curl request example of such a request to get all the records:

curl --location --request POST '${endpointURL}/services/data/v63.0/ui-api/list-records/${SObjectType}/${ListViewName}' \
    --header 'Content-Type: application/json' \
    --header 'Authorization: ${AccessToken}' \
    --data '{}'

Unfortunately, there is no direct Apex method for that, but we can make a callout to the UI API to perform these actions. Is it the best solution? Probably not, but it is possible.

The lightning/uiListsApi adapter gives you more control and flexibility over your List Views in Salesforce.

As we can see it is much more than we had before, and it is a great addition to the platform!

But how we can use it? Let’s see some examples!

Examples

Query records based on user List View

Imagine that you need to search for records based on the user criteria. To achieve that, before Spring’25, you would need to develop custom LWC for filtering and sorting the records.

Now, with the new Salesforce features, your users can tap into the strength of List Views to filter, sort, and search for records with ease. The new function, getListRecordsByName, empowers you to query records based on the user’s List View.

But it doesn’t stop there. You can also pass the List View details to the Apex Controller. This allows you to generate the SOQL query based on the List View details, enabling a more targeted search for records (of course, as long as getListRecordsByName is not enough).

import { LightningElement, wire } from "lwc";
import { getListRecordsByName } from "lightning/uiListsApi";

import LEAD_OBJECT from "@salesforce/schema/Lead";

export default class Example extends LightningElement {
    @wire(getListRecordsByName, {
        objectApiName: LEAD_OBJECT,
        listViewApiName: "LeadListView",
        fields: ["Lead.Name", "Lead.Industry", "Lead.AnnualRevenue", "Lead.Phone"], // optional
        sortBy: ["Lead.Name"], //optional
    })
    records;
}

If this should happen in an Apex transaction, you can make a direct callout to the UI API (described above), to get the records based on the List View details.

    HttpRequest request = new HttpRequest();
    request.setEndpoint('${endpointURL}/services/data/v63.0/ui-api/list-records/Lead/LeadListView');
    request.setMethod('POST');
    request.setHeader('Authorization', 'Bearer ' + accessToken);
    request.setHeader('Content-Type', 'application/json');
    request.setBody('{}');

    Http http = new Http();
    HttpResponse response = http.send(request);
    if (response.getStatusCode() == 200) {
        // do something with the records
    }

It is not only saving time during development but also allows users to use the platform features that they are familiar with and reduce the maintenance cost of the custom components.

Setting Preferences for List View

Imagine that you have a lot of list views, and you want to set the same preferences for all of them. Before Spring’25, you would need to do it manually for each of them.

But now, you can create LWC that will use the updateListPreferences function to set the same preferences for all of them and save a lot of time. This way, your users can set proper column width, wrap, and order for all of them or just for the selected ones.

import { LightningElement, api } from "lwc";
import { updateListPreferences } from "lightning/uiListsApi";

import LEAD_OBJECT from "@salesforce/schema/Lead";
export default class UpdateListPreferences extends LightningElement {
    @api async updateListViewPrefs() {
        updateListPreferences({
            objectApiName: LEAD_OBJECT.objectApiName,
            listViewApiName: "LeadListView",
            columnWidths: {
                Industry: 200,
                AnnualRevenue: 200,
                Phone: 200,
                Name: 250,
            },
            columnWrap: {
                Industry: true,
                AnnualRevenue: true,
                Phone: true,
                Name: true,
            },
            columnOrder: [
                "Industry",
                "AnnualRevenue",
            ],
        })
        .then(...)
        .catch(...);
    }
}

Custom List View Display

Imagine that you would like to display List Views in a custom way, similar to how different display views, such as Kanban, work.

Using the new functions from the lightning/uiListsApi, you can create a custom LWC that will display the List Views in a custom way and allow users to interact with them in a different way, at the same time keeping all the List View details compatible between other views.

To achieve that, you can:

  1. Create a custom LWC component that will display the records in a custom way,
  2. Use the getListInfoByName function in LWC to get the List Views details,
  3. Use getListRecordsByName or Apex Controller to get the records based on the List View,
  4. Use the updateListPreferences function to set the user preferences, filters, and sorting for the List View, to keep the List View up to date.
  5. Expose the custom LWC component for Lightning App Page as showed below:,
    // customView.js-meta.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>63.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Custom View</masterLabel>
    <description>New view for viewing the records.</description>
    <targets>
        <target>lightning__AppPage</target>
    </targets>
    </LightningComponentBundle>
  6. Create a custom Lightning Page (called CustomView) of App Page type and add the custom LWC to it,
  7. Create custom button on the Object and select List Button display type,
  8. Provide the URL to the custom lightning page from point 7 (/lightning/c/CustomView is a reference to custom Lightning Page).

Voilà, this will allow users to switch between the custom view and the standard List View.

Demo

Implementation

Below is an example of how the implementation could look:

// customListView.html
<template>
    <lightning-spinner lwc:if={showSpinner}></lightning-spinner>
    <lightning-card lwc:else>
        <lightning-button
            label="Search"
            variant="brand"
            slot="actions"
            onclick={handleSearch}
        ></lightning-button>
        <lightning-button
            label="Go to List View"
            slot="actions"
            onclick={handleGoToListView}
        ></lightning-button>
        <div class="slds-grid slds-p-around_small">
        <div class="slds-col slds-size_1-of-2 slds-p-right_small">
            <lightning-dual-listbox
                name="industry"
                label="Industry"
                value={selectedIndustries}
                placeholder="Select Industry"
                options={industryOptions}
                onchange={handleIndustryChange}
            ></lightning-dual-listbox>
        </div>
        <div class="slds-col slds-size_1-of-2">
            <lightning-input
                type="number"
                label="Min Annual Revenue"
                value={minRevenue}
                onchange={handleMinRevenueChange}
            ></lightning-input>
            <lightning-input
                type="number"
                label="Max Annual Revenue"
                value={maxRevenue}
                onchange={handleMaxRevenueChange}
            ></lightning-input>
        </div>
        <div class="slds-grid slds-wrap">
            <template for:each={records} for:item="record">
                <div
                key={record.Id}
                class="slds-col slds-size_1-of-5 slds-m-bottom_small"
                >
                    <lightning-card title="Custom Lead View" icon-name="custom:custom14">
                        <div class="slds-p-around_small" style="width: 300px;">
                        <p>Name: {record.Name}</p>
                        <p>Annual Revenue: {record.AnnualRevenue}</p>
                        <p>Industry: {record.Industry}</p>
                        <p>
                            Phone: <lightning-formatted-phone value={record.Phone}></lightning-formatted-phone>
                        </p>
                        </div>
                    </lightning-card>
                </div>
            </template>
        </div>
    </lightning-card>
</template>
// customListView.js
import { LightningElement, wire, track } from "lwc";
import { getListInfoByName, updateListInfoByName } from "lightning/uiListsApi";
import { NavigationMixin } from "lightning/navigation";

import LEAD_OBJECT from "@salesforce/schema/Lead";

import getRecords from "@salesforce/apex/CustomViewController.getRecords";

export default class CustomView extends NavigationMixin(LightningElement) {
    records = [];
    @track selectedIndustries = [];
    minRevenue;
    maxRevenue;
    showSpinner = true;

    @wire(getListInfoByName, {
        objectApiName: LEAD_OBJECT.objectApiName,
        listViewApiName: "Private",
    })
    wiredListInfo({ error, data }) {
        if (data) {
            const industryFilter = data.filteredByInfo.filter(
                (filter) => filter.fieldApiName === "Industry"
            );
            if (industryFilter.length > 0) {
                this.selectedIndustries = industryFilter[0].operandLabels;
            }
            const revenueFilter = data.filteredByInfo.filter(
                (filter) => filter.fieldApiName === "AnnualRevenue"
            );
            if (revenueFilter.length > 0) {
                revenueFilter.forEach((filter) => {
                if (filter.operator === "GreaterOrEqual") {
                    this.minRevenue = Number(
                        filter.operandLabels[0].replace(/\s/g, "")
                    );
                } else if (filter.operator === "LessOrEqual") {
                    this.maxRevenue = Number(
                        filter.operandLabels[0].replace(/\s/g, "")
                    );
                }
                });
            }
            this._getRecords();
            this.showSpinner = false;
        } else if (error) {
            console.error(error);
        }
    }

    handleSearch() {
        this._getRecords();
    }

    handleGoToListView() {
        const filteredByInfo = [];
        if (this.selectedIndustries.length > 0) {
            filteredByInfo.push({
                fieldApiName: "Industry",
                operator: "Contains",
                operandLabels: this.selectedIndustries,
            });
        }
        if (this.minRevenue) {
            filteredByInfo.push({
                fieldApiName: "AnnualRevenue",
                operator: "GreaterOrEqual",
                operandLabels: [String(this.minRevenue)],
            });
        }
        if (this.maxRevenue) {
            filteredByInfo.push({
                fieldApiName: "AnnualRevenue",
                operator: "LessOrEqual",
                operandLabels: [String(this.maxRevenue)],
            });
        }
        updateListInfoByName({
            objectApiName: LEAD_OBJECT.objectApiName,
            listViewApiName: "Private",
            filteredByInfo: filteredByInfo,
        }).then(() => {
            this[NavigationMixin.Navigate]({
                type: "standard__objectPage",
                attributes: {
                    objectApiName: LEAD_OBJECT.objectApiName,
                    actionName: "list",
                },
                state: {
                    filterName: "Private",
                },
            });
        });
    }

    handleIndustryChange(event) {
        this.selectedIndustries = event.detail.value;
    }

    handleMinRevenueChange(event) {
        this.minRevenue = event.detail.value;
    }

    handleMaxRevenueChange(event) {
        this.maxRevenue = event.detail.value;
    }

    get industryOptions() {
        return [
            { label: "Agriculture", value: "Agriculture" },
            { label: "Apparel", value: "Apparel" },
            { label: "Banking", value: "Banking" },
            { label: "Biotechnology", value: "Biotechnology" },
            { label: "Chemicals", value: "Chemicals" },
            ...
        ];
    }

    _getRecords() {
        getRecords({
            industries: this.selectedIndustries,
            minRevenue: this.minRevenue,
            maxRevenue: this.maxRevenue,
        })
        .then((result) => {
            this.records = result;
        })
        .catch((error) => {
            console.error("Error loading records", error);
        });
    }
}
// CustomViewController.cls
public with sharing class CustomViewController {
    @AuraEnabled
    public static List<Lead> getRecords(List<String> industries, Integer minRevenue, Integer maxRevenue) {
        List<String> whereClauses = new List<String>();
        if (industries != null && !industries.isEmpty()) {
            whereClauses.add('Industry IN :industries');
        }
        if (minRevenue != null) {
            whereClauses.add('AnnualRevenue >= :minRevenue');
        }
        if (maxRevenue != null) {
            whereClauses.add('AnnualRevenue <= :maxRevenue');
        }
        String query = 'SELECT Id, Name, Phone, AnnualRevenue, Industry FROM Lead';
        if (!whereClauses.isEmpty()) {
            query += ' WHERE ' + String.join(whereClauses, ' AND ');
        }
        return Database.query(query);
    }
}

Outro

The new lightning/uiListsApi adapter is a welcome upgrade to Salesforce. This tool not only allows you to perform all the Create, Read, Update, and Delete (CRUD) operations on List Views, but it also lets you customize your use of List Views in ways that weren’t possible before.

This adapter is a time-saver. It simplifies the development process, cutting down the time you need to spend on creating and maintaining custom components. Plus, it uses platform features that you’re already comfortable with.

The lightning/uiListsApi adapter is a practical and efficient addition to Salesforce. It’s designed to make working with List Views easier and more straightforward, so you can concentrate on using Salesforce to its full potential.

Mateusz Babiaczyk
Mateusz Babiaczyk
Salesforce Technical Architect
Technical Architect and skilled Full-stack Salesforce Developer since 2019. Achiever mindset, always ready to learn new technologies, and committed to ongoing self-improvement.

You might also like

LWC and Apex communication – the Ultimate Guide
January 15, 2024

LWC and Apex communication – the Ultimate Guide

See the ultimate guide for LWC and Apex connectivity solutions – how to get data, how to use wire and how to refresh cache – all in one place!

Krzysztof Pintscher
Krzysztof Pintscher

Salesforce Frontend Preacher

Advanced Promises in LWC
February 13, 2024

Advanced Promises in LWC

How and when to use Promise.all, allSettled, any, and race in LWC? Check out our latest post about advanced promises!

Piotr Gajek
Piotr Gajek

Senior Salesforce Developer