javascriptlit-elementlit-htmllit

lit-html: issue w/ using change event to propagate changes back to data model


In the below code, i am looping through a list of my data model (this.panel), and generating an input box for each. I would like to propagate any changes made by the user back to the model. to do this i am storing the index of the item im rendering in the html element, and adding a change event handler which will update the model using that index. The problem im having is the @change event at run time is throwing an error:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'updateModel')'

What's odd is i have another event handler, updateToggleState in a check box lower down that works fine. Any thoughts on what is going on here?

Here is the code: class Panel extends LitElement { static properties = { is_fetch_on_change: {}, };

    constructor() {
        super();
        this.panel = [
                            {
                                "id": "ticker",
                                "type": "str",
                                "place_holder": "ticker (i.e. YHOO:^NDX)",
                                "value": "YHOO:^NDX",
                                "fn_param_name": "ticker"
                            },
                            {
                                "id": "st_dt",
                                "type": "datetime",
                                "place_holder": "st_dt (i.e. 2012-01-01)",
                                "value": "2012-01-01",
                                "fn_param_name": "st_dt"
                            },
                            {
                                "id": "end_dt",
                                "type": "datetime",
                                "place_holder": "end_dt (i.e. 2022-01-01)",
                                "value": "",
                                "fn_param_name": "end_dt"
                            }
                    ]
        this.is_fetch_on_change = false;
    }

    render() {
        let ret_html = []
        
        this.panel.forEach(function(c, i){
            ret_html.push(html`
                <div class="m-2">
                    <label>${c.fn_param_name}</label>
                        <input type="text" class="form-control" id = "${c.id}" placeholder="${c.place_holder}" value="${c.value}" 
                            idx="${i}" @change=${this.updateModel} />
                    </div>
                </div>
            `)
        })

        ret_html.push(html`
            <div class="m-2">
            <label class="form-check-label" for="flexCheckDefault">
                <input class="form-check-input" type="checkbox" @change=${this.updateToggleState}>
                Refresh on change
            </label>
            </div>
            <button type="submit" class="btn btn-primary m-2" ?hidden=${this.is_fetch_on_change}>Submit</button>
        `)
        return ret_html;
    }

    updateModel(e){
        let idx = e.target.getAttribute('idx');
        //add code here to update model
    }

    updateToggleState(e){
        this.is_fetch_on_change = e.target.checked;
    }
}

customElements.define('lit-panel', Panel);

Update: I was able to solve the problem of not being able to reference 'this' from within the foreach loop. i needed to basically 'pass it in'. so with that solved, i can update the specific item of the dictionary now.
However, that does not update the element or re-render it.

Update 2: this is what i have now. i've tested this and it does add an element to this._panel_data when text input is changed. that change however is still not reflected in the UI.

class Panel extends LitElement {
    static properties() {
        _panel_data:{state: true}
      };

    constructor() {
        super();
        this.id = 100;
        this._panel_data = [{"id":"ticker","type":"str","place_holder":"ticker (i.e. YHOO:^NDX)","value":"YHOO:^NDX","fn_param_name": "ticker"},
                           {"id":"st_dt","type": "datetime","place_holder": "st_dt (i.e. 2012-01-01)","value": "2012-01-01","fn_param_name": "st_dt"}]
    }

    render() {
        return html`<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">

            ${this._panel_data.map((c, i) => 
                html`
                    <div class="m-2">
                        <label>${c.fn_param_name}</label>
                            <input type="text" class="form-control" id = "${c.id}" placeholder="${c.place_holder}" value="${c.value}" 
                                idx="${i}" .ref_obj=${c} @change=${e => this.updateModel(e)} />
                            <!--test if changes made to above, are reflected here...-->
                            <input type="text" class="form-control" value="${c.value}" />
                        </div>
                    </div>
                `
            )}
        `
    }

    updateModel(e){
        let clone = {...e.target.ref_obj};
        clone.value = e.target.value;
        this._panel_data = [...this._panel_data, clone];
    }
}

customElements.define('lit-panel', Panel);

Solution

  • Using an arrow function would solve the this problem

    this.panel.forEach((c, i) => {
      // `this` will be bound within here
    });
    

    Or you can use a regular for loop too.

    Since panel is not specified as a reactive property, changing that will not automatically re-render the component. Perhaps you want to make that into an internal reactive state.

    Keep in mind that since it is an array, just mutating it with this.panel[idx] = newValue won't trigger a re-render since the array reference will be the same. You need to create a new array reference or call this.requestUpdate(). More explained here https://lit.dev/docs/components/properties/#mutating-properties