Сделать атрибут оболочки реактивным в LWC

Aug 19 2020

Я подробно остановлюсь на проблеме, с которой сталкиваюсь, поэтому, если у меня есть совершенно лучший подход к реализации того, что я делаю, кто-то может это предложить.

Я создаю страницу сообщества Lightning, чтобы показать кейсы пользователей. Страница разделена на 3 столбца. В крайнем левом столбце показаны все дела, сделанные пользователем (вместе с темой, статусом и номером дела), в среднем столбце показаны комментарии к делу, выбранному в крайнем левом столбце, в крайнем правом столбце показаны немного более подробные сведения о случае, выбранном в крайнем левом столбце.

См. Изображение ниже.

Суть проблемы в том, что когда кто-то нажимает кнопку «Закрыть дело» в крайнем правом столбце, он вызывает сервер, чтобы изменить статус на «Закрыто». Это работает нормально, но затем мне нужно обновить статус, отображаемый в крайнем левом столбце, и там, где у меня возникают проблемы.

Это детали реализации.

Каждый столбец является компонентом, и все 3 столбца находятся в компоненте контейнера.

Код для компонента контейнера -

HTML компонента контейнера

<template>
   <div>
    <lightning-layout>
            <lightning-layout-item size="4">
                    <c-ma-my-cases-list-view oncaseselect={handlecaseselect} ></c-ma-my-cases-list-view>
            </lightning-layout-item>
            
            <lightning-layout-item size="4">
                    <c-ma-my-cases-comment selectedcaseid={selectedcaseid}></c-ma-my-cases-comment>
            </lightning-layout-item>

            <lightning-layout-item size="4">
                   <c-ma-my-cases-detail selectedcaseid={selectedcaseid}></c-ma-my-cases-detail>                  
            </lightning-layout-item>

    </lightning-layout>
  </div>
</template>

JS контейнерного компонента

import { LightningElement, track } from 'lwc';

export default class MaMyCasesContainer extends LightningElement {
    selectedcaseid;

    handlecaseselect(event){
        const caseId = event.detail;
        this.selectedcaseid = caseId;
    }
}

Подробная информация о крайнем левом компоненте - он извлекает случаи с сервера в настраиваемой структуре данных, перебирает их и использует дочерний компонент для отображения отдельных элементов.

HTML самого левого компонента maMyCasesListView

<template>
    <div class="container slds-p-around_large slds-scrollable_y slds-theme_shade">
        <div>
            <!-- Server call successful logic-->
            <template if:true={caseswrapper}>
                <lightning-layout class="slds-grid slds-grid_vertical" horizontal-align="left">
                    <template for:each={caseswrapper} for:item="currentcase">

                        <c-ma-case-list-item
                            data-id={currentcase.caseId}
                            key={currentcase.caseId}
                            macasewrapper={currentcase}
                            onselect={handleSelect}
                        ></c-ma-case-list-item>
                        
                    </template>
                </lightning-layout>
            </template>
        </div>      
    </div>
</template>

JS крайнего левого компонента maMyCasesListView

import { LightningElement, wire, track, api } from 'lwc';
import getCaseList from '@salesforce/apex/MA_MyCasesController.getCaseWrapperList';

export default class MaMyCasesListView extends LightningElement {

    selectedcasewrapper;
    @track caseswrapper;
    @track error;
    @track isLoading;


    constructor(){
        super();
        this.isLoading = true;
        getCaseList()
            .then(result =>{
                this.caseswrapper = result;
                this.error = undefined;
                this.isLoading = false;
            })
            .catch(error => {
                this.error = error;
                this.caseswrapper = undefined
                this.isLoading = false;
                this.showErrorToast();
            })
    }

    handleSelect(event) {
        const caseId = event.detail;
        this.toggleListItems('selected', caseId);
        this.selectedcasewrapper = this.caseswrapper.find(
            (macase) => macase.caseId === caseId
        );
        const selectEvent = new CustomEvent('caseselect', {
            detail: caseId
        });
        // Fire the custom event
        this.dispatchEvent(selectEvent);
    }
}

Как видно из html вышеупомянутого компонента, есть внутренний компонент, который показывает отдельный элемент списка.

HTML-код компонента, отображающего отдельный пункт списка дел maCaseListItem

<template>
    <div onclick={handleClick}>
        <lightning-layout horizontal-align="left" class="slds-grid slds-grid_vertical slds-text-align--left">
            <lightning-layout-item horizontal-align="left" class="slds-text-align--left">
                <b>{macasewrapper.Subject}</b>
            </lightning-layout-item>
            <lightning-layout-item class="slds-text-align--left slds-m-top_small">
                <lightning-layout>
                    <lightning-layout-item  horizontal-align="left" size="4">{macasewrapper.Status}</lightning-layout-item>
                    <lightning-layout-item horizontal-align="right">{macasewrapper.CaseNumber}</lightning-layout-item>
                </lightning-layout>
            </lightning-layout-item>
        </lightning-layout>
    </div>   
</template>

JS компонента, отображающего отдельный элемент списка дел maCaseListItem

import { LightningElement, api } from 'lwc';

export default class MaCaseListItem extends LightningElement {
    @api macasewrapper;
    
    handleClick(event) {

        const selectEvent = new CustomEvent('select', {
            detail: this.macasewrapper.caseId
        });
        // Fire the custom event
        this.dispatchEvent(selectEvent);
    }
}

Итак, крайний левый компонент показывает список случаев. Когда пользователь щелкает по одному из них, компонент maCaseListItem запускает событие caseselect с идентификатором дела в качестве детали события. Компонент контейнера улавливает его в функции handlecaseselect и сохраняет в атрибуте selectedcaseid.

Теперь компонент-контейнер передает этот идентификатор второму и третьему компонентам, чтобы они могли получить больше информации с сервера и показать соответствующие данные. Я пропущу второй компонент, так как он показывает комментарии к случаю. Перейдем к третьему компоненту, у которого есть кнопка Close case. Я сосредотачиваюсь только на кнопке закрытия дела и удаляю другой код для краткости

HTML компонента третьего столбца maMyCasesDetail

<template>
    <div if:true={localSelectedCaseId} >
    <div class="container slds-p-around_large slds-scrollable_y">
    <lightning-card>
        <!-- code to show status, created date etc -->
    </lightning-card>
    <lightning-card>
        <!-- code about showing attachments -->
    </lightning-card>
    <div class="slds-box slds-align_absolute-center slds-theme_default">
       <lightning-button variant="brand-outline" label="Close Case" title="Close the case" onclick={handleCloseClick}></lightning-button>
    </div>    
    </div> 
</div>
</template>

JS компонента третьего столбца maMyCasesDetail

import { LightningElement, api, track, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import closeCase from '@salesforce/apex/MA_MyCasesController.closeCase';

export default class MaMyCasesDetail extends LightningElement {


    @track localSelectedCaseId;

    @api
    get selectedcaseid() {
        return this.localSelectedCaseId;
    }
    set selectedcaseid(value) {
        this.localSelectedCaseId = value;
    }


    @wire(getRecord, { recordId: '$localSelectedCaseId', fields: [CASENUMBER_FIELD, CASESUBJECT_FIELD, STATUS_FIELD, DESCRIPTION_FIELD, 'Case.Owner.Name', 'Case.CreatedDate'] })
    wiredRecord({ error, data }) {
        //code that gets case details from server based on localSelectedCaseId
    }


    handleCloseClick(event) {
        if (this.localSelectedCaseId && this.status != this.label.CaseClosedStatus) {
            this.isLoaded = !this.isLoaded;
            closeCase({ caseId: this.localSelectedCaseId })
                .then(result => {
                    this.caseStatus = result.Status;
                    this.handleSuccess();
                    this.status = result.Status;
                    this.sendCloseCaseDetails();
                })
                .catch(error => {
                    var errorMsg = 'Unknown error';
                    if (error.body) {
                        errorMsg = error.body.message;
                        console.log(errorMsg);
                    }
                })
        } 
    }

    sendCloseCaseDetails() {
        var caseClosedDetails = {status:this.status, id:this.localSelectedCaseId};
        const closeEvent = new CustomEvent('close', {
            detail: caseClosedDetails
        });
        this.dispatchEvent(closeEvent);
    }

}

Таким образом, когда пользователь нажимает кнопку «Закрыть дело», серверный метод обновляет статус до «Закрыто» и возвращает обновленное дело. Этот компонент запускает событие, содержащее идентификатор дела и обновленный статус. Родительский компонент может это поймать, но после этого я не могу заставить что-то работать. Родитель может передать их как параметры крайнему левому компоненту.

@AuraEnabled
public static Case closeCase(String caseId) {
    Case c = [Select Id, Status from Case where Id=:caseId];
    c.Status = "Closed";
    update c;
    return c;   
}

код на стороне сервера, который получает начальный список случаев (для самого левого компонента):

@AuraEnabled
public static List<CaseCommentWrapper> getCaseCommentWrapperList(String caseId){
    User currentUser= [select Id,timezonesidkey from user where Id =:UserInfo.getUserId()];
    List<CaseComment> allCaseComments = [Select Id, ParentId, IsPublished, CommentBody, CreatedById, CreatedBy.Name, CreatedDate, SystemModstamp, LastModifiedDate, LastModifiedById, IsDeleted From CaseComment where ParentId=:caseId ORDER BY CreatedDate ASC];
    List<CaseCommentWrapper> allCaseCommentWrapper = new List<CaseCommentWrapper>();
    for(CaseComment c : allCaseComments){
        String createdDateStringFormat = getDateInStringFormat(c.CreatedDate,currentUser.timezonesidkey);
        Boolean isCurrentUserAuthor = (currentUser.Id == c.CreatedById)?True:False;
        String caseCommentId = String.ValueOf(c.Id);
        allCaseCommentWrapper.add(new CaseCommentWrapper(caseCommentId,c.CommentBody, c.CreatedBy.Name, createdDateStringFormat,isCurrentUserAuthor));
    }
    return allCaseCommentWrapper;
}

public class CaseWrapper{
    @AuraEnabled
    public Id caseId;
    @AuraEnabled
    public String CaseNumber;
    @AuraEnabled
    public String Status;
    @AuraEnabled
    public String Subject;

    public CaseWrapper(Id caseId,String CaseNumber, String Status, String Subject){
        this.caseId = caseId;
        this.CaseNumber = CaseNumber;
        this.Status = Status;
        this.Subject = Subject;
    }
}

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

Какой хороший способ обновить статус этого конкретного дела в HTML самого левого компонента? Я попробовал, где родительский компонент обрабатывает событие, установил 2 атрибута в компоненте maMyCasesListView (крайний левый столбец) - один атрибут, который отображает идентификатор случая (localcasestatuschangeid), другой атрибут, который отображает обновленный статус (localcasestatuschangeupdatedstatus). Я пробовал перебирать каждый элемент списка внутри maMyCasesListView, чтобы увидеть, какой из них соответствует идентификатору дела, и обновить статус. Но это не отображается в HTML.

this.template.querySelectorAll('c-ma-case-list-item').forEach(item => {
    console.log('iterating through list');
    if (item.macasewrapper.caseId === this.localcasestatuschangeid) {
        console.log('case found');
        item.macasewrapper.Status = this.localcasestatuschangeupdatedstatus;
    }
});

Но, как я уже сказал, он не отображается в HTML. Я даже считаю, что идея иметь два атрибута - localcasestatuschangeid и localcasestatuschangeupdatedstatus - в компоненте maMyCasesListView не очень яркая. Событие, обрабатываемое родителем, - это объект, в котором у event.detail есть набор параметров. Есть ли способ просто передать event.detail из компонента контейнера в maMyCasesListView, а затем получить оттуда идентификатор и статус maMyCasesListView?

Ответы

rahulgawale Aug 20 2020 at 07:33

Есть два способа сделать это, но перед этим вам нужно сделать одно изменение, вам нужно изменить getCaseListметод на проводной метод апекса вместо того, чтобы вызывать его императивно в конструкторе.

caseListWired; // define new property

@wire(getCaseList)
getCaseListWired(value) {
    this.caseListWired = value;
    let { error, data } = value;
    if (error) {
        this.error = error;
        this.caseswrapper = undefined
        this.isLoading = false;
        this.showErrorToast();
    } else if (data) {
        this.caseswrapper = result;
        this.error = undefined;
        this.isLoading = false;
    }
}

Также вам нужно импортировать refreshApex.

import { refreshApex } from '@salesforce/apex';

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

Подход 1

Шаг 1

Написать один публичный метод в крайнем левом компоненте , т.е. maMyCasesListView.

@api refreshCaseList(){
    refreshApex(this.caseListWired); // this is the same attribute assigned in wired method.
}

Шаг 2

Отправьте событие из крайнего правого компонента maMyCasesDetail, когда дело закрыто, что вы уже сделали в handleCloseClickметоде.

const closeEvent = new CustomEvent('close');
this.dispatchEvent(closeEvent);

Обратите внимание, что теперь вам не нужно отправлять какие-либо данные.

Шаг 3

Вам нужно обработать это событие в компоненте контейнера и вызвать общедоступный метод, который мы определили на шаге 1.

 <c-ma-my-cases-detail selectedcaseid={selectedcaseid} onclose={handleCloseCase}></c-ma-my-cases-detail>

JS

handleCloseCase(){
    this.template.querySelector('c-ma-my-cases-list-view').refreshCaseList();
}

Подход 2

Вы можете использовать события pub-sub, то есть Lightning Messaging Service, для вызова метода refreshApex.

Предложения

Поскольку вы просто обновляете статус дела, вы используете updateRecordfrom uiRecordApiдля обновления статуса вместо настраиваемого метода апекса.

Если у вас нет особой необходимости, вы можете удалить оболочку и вернуть список дел в getCaseListметоде.