Search code examples
polymerlit-element

Update elements property binding


I have an LitElement based web component, that renders a mdw-dialog with a custom element inside that uses a property from the parent component. When user click an a button, the dialog is shown that renders a couple of input fields. This all works great, but I want the property to be reset to an empty object whenever the button is clicked, before the dialog is shown and though I can easily do so - it does not appear to have an effect on the dialog elements.

Parent element

export class PageApplications extends LitElement {

    static get properties() {
        return {
            application: { type: Object }
        }
    }

    constructor() {
        super();
        this.application = {};
    }

    onMouseOver(event) {
        this.shadowRoot.querySelector('mwc-fab').extended = ('mouseenter' === event.type);
    }

    openDialog() {
        console.log('PageApplication.openDialog:', this.application);
        this.application = {};
        let dialog = this.shadowRoot.querySelector('mwc-dialog');
        dialog.requestUpdate;
        dialog.open = true;
    }

    closedDialog(event) {
        console.log('closedDialog:', this.application)
        if ('ok' === event.detail.action) {
            // NOP
        }
    }

    render() {
        console.log('PageApplication.render', this.application);
        return html`
            <style type="text/css">
                mwc-fab {
                    position: fixed;
                    right: 1rem;
                }
                mwc-list {
                    background-color: #FFF;
                }
            </style>
            <mwc-fab showIconAtEnd icon="add_circle" label="New Application" 
                @mouseenter="${this.onMouseOver}"
                @mouseout="${this.onMouseOver}"
                @click="${this.openDialog}"
                >
            </mwc-fab>
            <mwc-dialog heading="Create Application"
                @closed="${this.closedDialog}">
                <cnvy-create-application
                    .application="${this.application}"
                ></cnvy-create-application>
                <mwc-button
                    slot="primaryAction"
                    dialogAction="ok"
                >Create</mwc-button>
                <mwc-button
                    slot="secondaryAction"
                    dialogAction="cancel"
                >Cancel</mwc-button>            
            </mwc-dialog>
            <h2>Applications</h2>

as you can see the application property is bound to element cnvy-create-application

export class ConvoyCreateApplication extends LitElement {

    constructor() {
        super();
    }

    static get properties() {
        return {
            application: { type: Object }
        }
    }

    render() {
        console.log('ConvoyCreateApplication.render', this.application);
        return html`
            <style>
                mwc-formfield {
                    display: block;
                }
            </style>
            <mwc-formfield>
                <mwc-textfield 
                    placeholder="Name" 
                    helper="Name of application"
                    .value="${this.application.name}"
                    @change="${e => this.application.name = e.target.value}"
                    ></mwc-textfield>
            </mwc-formfield>
            <mwc-formfield>
                <mwc-textarea 
                    rows="2" 
                    placeholder="Description" 
                    helper="Short description"
                    .value="${this.application.description}"
                    @change="${e => this.application.description = e.target.value}"
                ></mwc-textfield>
            </mwc-formfield>
        `;
    }

}
customElements.define('cnvy-create-application', ConvoyCreateApplication);

Problem is - the first time I click the button, the dialog is shown as expected and each textfield says "undefined", which is probably ok as props are not defined in empty object. I enter value respectively "a" and "b" and click ok in the dialog and console prints

closedDialog: {name: "a", description: "b"}

as expected. If I then click button again, the "application" property should be reset to empty object but UI shows textfields with "a" and "b", and if I change "b" to "d", only "d" is sent back

closedDialog: {description: "d"}

meaning the application object was infact reset to empty, BUT this is not what is shown ... so, how do I trigger the update? I've tried various "update", "requestUpdate" .. and such lifecycle methods but to no avail ..


Solution

  • mwc-textfield internally uses a native input and passes the value property binding to it. This is one of the cases where a property is updated from multiple sources (from .value="${this.application.name}" and from the DOM when editing the text). In this scenario lit-html may not know how to update the value and can cause a misalignment between the data and what's actually shown. To solve this you can use the live directive: it

    checks binding value against the live DOM value, instead of the previously bound value, when determining whether to update the value.

    import {live} from 'lit-html/directives/live';
    
    // ...
    
    render() {
      return html`
        <mwc-textfield
          .value=${live(this.application.name)}
                   ^^^^
        ></mwc-textfield>
      `;
    }
    

    In openDialog() you have probably missed the parenthesis in dialog.requestUpdate˅; although there's no need to call it: this.application = {}; is sufficient to trigger an update.

    To prevent the textfields from showing undefined you can either provide a fallback value in the binding

    <mwc-textfield
      .value=${live(this.application.name || '')}
    ></mwc-textfield>
    

    or initialize the entire data structure with empty values:

    constructor() {
      this.application = {
        a: '',
        b: '',
        // ...
      };
    }