4

I'm trying to write a Lightning Web Component (yay, first one).

Still in the "proof of concept; can I get user input to propagate around the page?" stage.

I have a List<MyApexWrapperClass> whose records I'd like to serve as rows of an HTML table (and whose @AuraEnabled properties such as value_for_freeform_column I'd like to have serve as the columnar cell values).

I got the List<MyApexWrapperClass into a JavaScript controller helloworld.js property wrapped_records as follows just fine:

import { LightningElement, track } from 'lwc';
import lwcMagic from '@salesforce/apex/MyWrapperFetcher.lwcMagic';

export default class helloworld extends LightningElement {
    @track wrapped_records;

    doMagic() {
        lwcMagic()
            .then(result => {
                this.wrapped_records = result;
                this.error = undefined;
            })
            .catch(error => {
                this.error = error;
                this.wrapped_records = undefined;
            });
    }

}

I'm working with a situation where I've got @AuraEnabled properties of an Apex class serving as the basis of a JavaScript controller variable I called wrapped_records (note that that's plural -- I'm fetching from an Apex method that returns a List<MyWrapperClass>). Then between <tbody>...</tbody> tags I have a <template for:each={wrapped_records} for:item="w_record"> tagset where there's a <tr key={w_record.unique_name}> within, and td's within those. In the td's, I am trying to do something along the following lines:

<lightning-input 
  type="text" 
  label="Need to enter a manual value?" 
  variant="label-hidden"
  value={w_record.value_for_freeform_column}
  placeholder="type here..."
>
</lightning-input>
You entered:  <em>{w_record.value_for_freeform_column}</em>

The full HTML template helloworld.html is as follows:

<template>
    <template if:false={wrapped_records}>
        <lightning-button
            label="Do The Magic"
            onclick={doMagic}
        ></lightning-button>
    </template>
    <template if:true={wrapped_records}>
        <table class="slds-table slds-table_bordered slds-border_left slds-border_right">
            <thead>
                <tr class="slds-line-height_reset">
                    <th class="" scope="col">
                        <div class="slds-truncate" title="Unique Name">Unique Name</div>
                    </th>
                    <th class="" scope="col">
                        <div class="slds-truncate" title="Freeform">Freeform</div>
                    </th>
                </tr>
            </thead>
            <tbody>
                <template for:each={wrapped_records} for:item="w_record">
                    <tr key={w_record.unique_name}>
                        <th data-label="UniqueName" scope="row" key={w_record.unique_name}>
                            <div class="slds-truncate" title={w_record.unique_name}><b><u>{w_record.unique_name}</u></b></div>
                        </th>
                        <td data-label="Freeform" title={w_record.unique_name} key={w_record.unique_name}>
                            <div class="slds-truncate">
                                <lightning-input 
                                    type="text" 
                                    label="Need to enter a manual value?" 
                                    variant="label-hidden"
                                    value={w_record.value_for_freeform_column}
                                    placeholder="type here..."
                                >
                                </lightning-input>
                                You entered:  <em>{w_record.value_for_freeform_column}</em>
                            </div>
                        </td>
                    </tr>
                </template>
            </tbody>
        </table>
    </template>
    <template if:true={error}>
        There was an error.
    </template>
</template>

So far, my resulting table looks like this:

| Unique Name | Freeform                                       |
| ----------- | ---------------------------------------------- |
| ----------- | ---------------------------------------------- |
| ABC         | (Editable input box displaying "Hello World!") |
|             | You entered: Hello World!                      |
| ----------- | ---------------------------------------------- |
| JKL         | (Editable input box displaying "Hello World!") |
|             | You entered: Hello World!                      |
| ----------- | ---------------------------------------------- |
| XYZ         | (Editable input box displaying "Hello World!") |
|             | You entered: Hello World!                      |
| ----------- | ---------------------------------------------- |

(Note: a MyApexWrapperClass object always instantiates with a value_for_freeform_column value of Hello World!)


What I'd like to be able to do is backspace out the "Hello World!" in row "JKL" and type "Goodbye," and as I type, the "You entered:" immediately underneath it updates and displays, You entered: Goodbye when I finish typing.

| Unique Name | Freeform                                       |
| ----------- | ---------------------------------------------- |
| ----------- | ---------------------------------------------- |
| ABC         | (Editable input box displaying "Hello World!") |
|             | You entered: Hello World!                      |
| ----------- | ---------------------------------------------- |
| JKL         | (Editable input box displaying "Goodbye")      |
|             | You entered: Goodbye                           |
| ----------- | ---------------------------------------------- |
| XYZ         | (Editable input box displaying "Hello World!") |
|             | You entered: Hello World!                      |
| ----------- | ---------------------------------------------- |

I read https://lwc.dev/guide/html_templates#handle-user-input, but it seems to presume that there's a globally scoped variable used as the value for your <lightning-input> (e.g. in this.name = event.target.value;).

My <lightning-input>'s value, however, is a temporary loop reference to a piece of an array variable ({w_record.value_for_freeform_column}).

Is there some sort of code I could write that would let me write a handleInput(event)-like JavaScript controller function capable of assigning event.target.value to such a locally-scoped-in-the-HTML variable?

(Note: I'll want any edits to a property of w_record to either update the corresponding piece of wrapped_records or to be easy to fetch later, since in the bigger picture, my HTML table is a way of rendering a complex data entry form whose final values will need to be sent back to Apex as part of some complex DML operations when the user clicks "all done.")

1

2 Answers 2

2

As suggested by sfdcfox you can use, data attributes,

if you dont wanna use data attribute you can use lightning-input name attribute instead.

<template for:each={wrapped_records} for:item="w_record">
                    <tr key={w_record.unique_name}>
                        <th data-label="UniqueName" scope="row" key={w_record.unique_name}>
                            <div class="slds-truncate" title={w_record.unique_name}><b><u>{w_record.unique_name}</u></b></div>
                        </th>
                        <td data-label="Freeform" title={w_record.unique_name} key={w_record.unique_name}>
                            <div class="slds-truncate">
                                <lightning-input name={w_record.unique_name}
                                    type="text" 
                                    label="Need to enter a manual value?" 
                                    variant="label-hidden" onchange={updateFreeFormColum}
                                    value={w_record.value_for_freeform_column}
                                    placeholder="type here..."
                                >
                                </lightning-input>
                                You entered:  <em>{w_record.value_for_freeform_column}</em>
                            </div>
                        </td>
                    </tr>
                </template>

JS:

import { LightningElement, track, api } from 'lwc';

export default class App extends LightningElement {
    @track wrapped_records = [{unique_name:'ABC' , value_for_freeform_column : 'Hello World'} ,
     {unique_name:'JKL' ,value_for_freeform_column : 'Hello World'}  , 
     {unique_name:'XYZ' , value_for_freeform_column : 'Hello World'}];

     updateFreeFormColum(event){
        console.log(event.target.name);

        var foundElement = this.wrapped_records.find(function(element){
            return element.unique_name === event.target.name;
        });        
        foundElement.value_for_freeform_column = event.target.value;

     }
}

Playground Link: https://developer.salesforce.com/docs/component-library/tools/playground/K88RmBKA6/1/edit

3
  • Thanks -- followup question. Is accessing the equivalent of element.unique_name inside the .find() function going to need to be different when wrapped_records comes out of the result of an Apex returning a List<SomeObject> than when it's written as a JSON literal like in your example? The code works, then element.unique_name goes undefined if I try to wire back in the Apex. It looks like it's buried deep in some sort of Proxy and originalTarget structure?
    – k..
    Nov 13, 2019 at 17:23
  • its a proxy right, used for caching, if you use console.log(JSON.stringify(this.wrapped_records)); you will see your records in JSON format Nov 13, 2019 at 17:27
  • 1
    My errors flipping between the literal-defined wrapped_records and the Apex-populated version ended up being some unrelated typos ... thanks for the stringify() tip!
    – k..
    Nov 13, 2019 at 19:08
5

Here's a Playground that demonstrates how to set an onchange handler like what you're asking for:


import { LightningElement, track } from 'lwc';

export default class App extends LightningElement {
    @track rows;
    connectedCallback() {
        this.rows = [{id:0,value:''},{id:1,value:''},{id:2,value:''}];
    }
    handleChange(event) {
        this.rows[event.target.closest('[data-key]').dataset.key].value = event.target.value;
    }
}

<template>
    <template for:each={rows} for:item="item">
        <div key={item.id} data-key={item.id}>
            <input onchange={handleChange} value={item.value} /><br />
            "{item.value}"
        </div>
    </template>
</template>

Basically, we assign the key to an item we can inspect (data-key), then use that to update the array in the controller.

3
  • What's the connectedCallback() for, and what exactly are you setting with 0, 1, and 2?
    – k..
    Nov 13, 2019 at 16:25
  • @k.. connectedCallback is a special method that is called when a component is loaded. The values 0, 1, and 2 are just index values in to the array. You could do it other ways, too, like by a record Id, then you'd need to use Array.prototype.find instead. This is just a simple self-contained example.
    – sfdcfox
    Nov 13, 2019 at 16:34
  • I got Pranay's variation working a little faster than this one made sense to me at my skill level for JavaScript, so I accepted that one, but I appreciate you being so fast on the ball -- also great information to know, thanks!
    – k..
    Nov 13, 2019 at 19:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .