Rendi reattivo l'attributo wrapper in LWC

Aug 19 2020

Elaborerò più del necessario il problema che sto affrontando, in modo che se c'è un approccio completamente migliore per implementare ciò che sto facendo, qualcuno può suggerirlo.

Sto costruendo una pagina della community fulminea per mostrare i casi di un utente. La pagina è divisa in 3 colonne. La colonna più a sinistra mostra tutti i casi dell'utente (insieme a oggetto, stato e numero di caso), la colonna centrale mostra i commenti sul caso selezionato nella colonna più a sinistra, la colonna più a destra mostra alcuni dettagli del caso selezionato nella colonna più a sinistra.

Si prega di vedere l'immagine qui sotto.

Il punto cruciale del problema è che quando qualcuno fa clic sul pulsante "Chiudi caso" nella colonna più a destra, chiama il server per modificare lo stato in "Chiuso". Funziona bene, ma poi devo aggiornare lo stato mostrato nella colonna più a sinistra e quello in cui ho problemi.

Questi sono i dettagli dell'implementazione.

Ciascuna colonna è un componente e tutte e 3 sono in un componente contenitore.

Codice per il componente contenitore -

HTML del componente contenitore

<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 del componente contenitore

import { LightningElement, track } from 'lwc';

export default class MaMyCasesContainer extends LightningElement {
    selectedcaseid;

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

Dettagli del componente più a sinistra: recupera i casi dal server in una struttura dati personalizzata, li ripete e utilizza un componente figlio per visualizzare i singoli elementi

HTML del componente più a sinistra 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 del componente più a sinistra 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);
    }
}

Come visibile dall'html del componente precedente, c'è un componente interno che mostra la singola voce dell'elenco.

HTML del componente che mostra l'elemento dell'elenco dei casi individuali 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 del componente che mostra l'elemento dell'elenco dei casi individuali 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);
    }
}

Quindi il componente più a sinistra mostra l'elenco dei casi. Quando un utente fa clic su uno di essi, un evento "caseelect" viene attivato dal componente maCaseListItem con ID caso come dettaglio dell'evento. Il componente contenitore lo cattura nella funzione handlecaseselect e lo memorizza nell'attributo selectedcaseid.

Ora il componente contenitore passa questo ID al secondo e terzo componente, in modo che possano recuperare più informazioni dal server e mostrare i dati rilevanti. Salterò il secondo componente, poiché mostra i commenti sui casi. Passiamo al terzo componente, che ha il pulsante Chiudi caso. Mi sto concentrando solo sul pulsante Chiudi caso e rimuovendo altro codice per brevità

HTML del componente della terza colonna 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 del componente della terza colonna 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);
    }

}

Pertanto, quando l'utente fa clic sul pulsante "Chiudi caso", il metodo lato server aggiorna lo stato su "Chiuso" e restituisce il caso aggiornato. Questo componente attiva un evento contenente l'ID del caso e lo stato aggiornato. Il componente genitore può prenderlo, ma dopo non riesco a far funzionare le cose. Il genitore può passarli come parametri al componente più a sinistra.

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

il codice lato server che ottiene l'elenco iniziale dei casi (per il componente più a sinistra) è -

@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;
    }
}

Quindi la terza (colonna più a destra) attiva un evento che ha l'ID del caso per cui lo stato è stato aggiornato e lo stato aggiornato come dettagli dell'evento.

Qual è il modo migliore per aggiornare lo stato di quel caso particolare sull'HTML del componente più a sinistra? Ho provato dove il componente genitore gestisce l'evento, ho impostato 2 attributi sul componente maMyCasesListView (colonna più a sinistra): un attributo che rappresenta l'ID del caso (localcasestatuschangeid), un altro attributo che descrive lo stato aggiornato (localcasestatuschangeupdatedstatus). Ho provato a scorrere ogni elemento dell'elenco all'interno di maMyCasesListView per vedere quale ID caso corrisponde e aggiornare lo stato. Ma non viene visualizzato in 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;
    }
});

Ma come ho detto, non viene visualizzato in HTML. Trovo persino che l'idea di avere 2 attributi - localcasestatuschangeid e localcasestatuschangeupdatedstatus - sul componente maMyCasesListView non sia molto brillante. L'evento gestito dal genitore è un oggetto, dove event.detail aveva una serie di parametri. C'è un modo in cui posso semplicemente passare event.detail dal componente contenitore a maMyCasesListView e quindi fare in modo che maMyCasesListView estragga l'ID e lo stato da lì?

Risposte

rahulgawale Aug 20 2020 at 07:33

Ci sono due modi per farlo, ma prima è necessario apportare una modifica, è necessario modificare il getCaseListmetodo in un metodo apex cablato invece di chiamarlo imperativamente nel costruttore.

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;
    }
}

Inoltre, devi importare refreshApex.

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

Con il metodo cablato, hai due vantaggi, uno è la riduzione delle chiamate al server e l'altro è che puoi aggiornare la cache ogni volta che è necessario utilizzando refreshApex.

Approccio 1

Passo 1

Scrivi un metodo pubblico nel componente più a sinistra, ad es maMyCasesListView.

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

Passo 2

Invia un evento dal componente più a destra maMyCasesDetail, quando il caso è chiuso, cosa che hai già fatto nel handleCloseClickmetodo.

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

Nota che ora non è necessario inviare alcun dato.

Passaggio 3

È necessario gestire quell'evento nel componente contenitore e chiamare il metodo pubblico che abbiamo definito nel passaggio 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();
}

Approccio 2

È possibile utilizzare gli eventi pub-sub, ad esempio Lightning Messaging Service, per chiamare il metodo refreshApex.

Suggerimenti

Poiché stai solo aggiornando lo stato del caso, utilizzi updateRecordda uiRecordApiper aggiornare lo stato invece del metodo dell'apice personalizzato.

Se non hai esigenze specifiche puoi eliminare il wrapper e restituire l'elenco dei casi nel getCaseListmetodo.