Редактируемая таблица на базе Lightning web component

Попадалась на одном легаси проекте задача: сделать на вижуалфорс странице табличку, показывающую список дочерних записей и позволяющую их редактировать без перехода на саму запись. Соответственно, страничка на лаяуте родителя. Особых проблем это не вызвало, довольно рутинная задача, но в какой-то момент стало интересно реализовать то же самое, но используя LWC.
В силу своей лени я решил не создавать для этого новые объекты на своей дэв организации (в оригинале было 2 кастомных), а просто вывести список контактов на аккаунте.
Вот что в итоге получилось.

Для того, чтобы вытащить нужные контакты, создал простенький класс, который только это и умеет:

public with sharing class CuteContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> getContactsByAccount(String accountId) {
        return [SELECT Id, FirstName, Lastname, Title, Phone, Email from Contact where AccountId = :accountId];
    }
}

Потом создал сам компонент. Традиционно, он состоит из 3х файлов: .html, .js и .js-meta.xml.
С последним всё понятно, версия АПИ - текущая, isExposed = true и в targets указываем lightning__RecordPage. Текст файла ниже:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>47.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

В файле с html разметкой на помощь приходит компонент lightning-datatable:

<template>
    <lightning-card title="Related Contacts" icon-name="custom:custom69">// nice
        <div class="slds-m-around_medium">
            <template if:true={contacts.data}> // valid contact data has been returned from an apex method
                <lightning-datatable
                    key-field="Id" // maps each row to a contact record
                    data={contacts.data}
                    columns={columns} // constant collection we'll set in js constructor
                    onsave={handleSave} // js method to handle event
                    draft-values={draftValues} // the place to store user input
                    hide-checkbox-column=true> 
                </lightning-datatable>
            </template>
            <template if:true={contacts.error}> // we're just playing so let's hope everything's gonna be ok:)
                <!-- if some error is appeared do something -->
            </template>
        </div>
    </lightning-card>
</template>

И, собственно, javascript контроллер:

import { LightningElement, wire, track, api } from 'lwc';
import getContactsByAccount from '@salesforce/apex/CuteContactController.getContactsByAccount';
import { updateRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

const COLUMNS = [
    { label: 'First Name', fieldName: 'FirstName', editable: true },
    { label: 'Last Name', fieldName: 'LastName', editable: true },
    { label: 'Title', fieldName: 'Title', editable: true },
    { label: 'Phone', fieldName: 'Phone', type: 'phone', editable: true },
    { label: 'Email', fieldName: 'Email', type: 'email', editable: true }
];

export default class RelatedContactsByForAccount extends LightningElement {
    @track columns = COLUMNS;
    @track draftValues = [];
    @api recordId;

    @wire(getContactsByAccount, {accountId:'$recordId'})
    contacts;

    handleSave(event) {

        const recordInputs =  event.detail.draftValues.slice().map(draft => {
            const fields = Object.assign({}, draft);
            return { fields };
        });

        const promises = recordInputs.map(recordInput => updateRecord(recordInput));

        Promise.all(promises).then(records => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Success',
                    message: 'Contact updated',
                    variant: 'success'
                })
            );
            this.draftValues = [];
            return refreshApex(this.contacts);
        }).catch(error => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error updating record',
                    message: error.body.message,
                    variant: 'error'
                })
            );
        });        
    }
}

COLUMNS описывает значения и поведение колонок нашей таблицы. В данном случае я хотел, чтобы все значения были редактируемыми, поэтому editable параметр выставлен в true для всех колонок. Само собой, это опционально.
Коллекция draftValues изначально пустая. В ней будут храниться изменения, внесённые пользователем.
recordId - айдишник текущего аккаунта.

Для вызова апекс метода, который мы заимпортили в начале, используем @wire, передаём ему recordId и помещаем результат в переменную contacts. Теперь она хранит коллекцию контактов, которую вернул апекс метод.

При изменении каких-либо значений в таблице, и нажатии кнопки Save (содержится в lightning-datatable компоненте, поэтому явно на странице не указана), ивент обрабатывается методом handleSave.

Тут стоит заметить, что метод updateRecord принимает только одну строку за раз. И если бы мы обрабатывали изменения только первой строки таблицы (странный пример, но для наглядности), то выглядело бы это как-то так:

const fields = {};
        fields[ID_FIELD.fieldApiName] = event.detail.draftValues[0].Id;
        fields[FIRSTNAME_FIELD.fieldApiName] = event.detail.draftValues[0].FirstName;
        fields[LASTNAME_FIELD.fieldApiName] = event.detail.draftValues[0].LastName;
//and all used fields

        const recordInput = {fields};

        updateRecord(recordInput)
        // and so on

Когда все записи успешно обновлены, на странице появляется сообщение об этом:

new ShowToastEvent({
                    title: 'Success',
                    message: 'Contact updated',
                    variant: 'success'
                })

Коллекция с изменениями очищается и футер lightning-datatable компонента с кнопками Cancel и Save пропадает:

this.draftValues = [];

И, что самое приятное, таблица обновляется. Апекс метод у нас cacheable, но в данном случае мы собственноручно обновили записи в базе данных, значит данные, сохранённые в кеше, уже не актуальны. Вызвать метод заново и получить свежую информацию можно с помощью метода refreshApex(wiredProperty), где wiredProperty - та самая переменная, над которой мы указывали аннотацию @wire. Апекс метод будет вызван заново и наша wired переменная обновится, а с ней и данные в нашей таблице.

1 Like