Search code examples
formspolymerpolymer-2.x

Resetting forms in Polymer 2.x


I am trying to reset my form. What am I doing wrong? What is best-practice?

Here is my Plunk demo.

My problem on the demo is that connectedCallback() appears to fire continually (not just on initial load), thereby losing the value of savedItem by updating it to newItem on each update.

Here is the same issue on Github.

https://plnkr.co/edit/wRdXXws2UXl3VXrycqua?p=preview

my-demo.html
<base href="https://polygit.org/polymer+v2.0.0/shadycss+webcomponents+1.0.0/components/">
<link rel="import" href="polymer/polymer-element.html">
<link rel="import" href="paper-toggle-button/paper-toggle-button.html">


<dom-module id="my-demo">
  <template>
    <style>
      :host > * {
        margin-top: 40px;
        font-size: 18px;
      }
      button.save {
        color: white;
        background-color: blue;
      }
    </style>

    <paper-toggle-button checked="{{item.alice}}">Alice</paper-toggle-button>
    <paper-toggle-button checked="{{item.bob}}">Bob</paper-toggle-button>
    <paper-toggle-button checked="{{item.charlie}}">Charlie</paper-toggle-button>
    <paper-toggle-button checked="{{item.dave}}">Dave</paper-toggle-button>

    <button>Reset</button>
    <button class="save" on-tap="_reset">Save</button>

  </template>

  <script>
    class MyDemo extends Polymer.Element {
      static get is() {
        return 'my-demo';
      }
      static get properties() {
        return {
          item: {
            type: Object,
            notify: true,
            value: () => {
              return {
                alice: false,
                bob: false,
                charlie: false,
                dave: true,
              };
            },
          },
          savedItem: {
            type: Object,
            notify: true,
          },
        };
      }

      connectedCallback() {
        super.connectedCallback();
        this.set('savedItem', this.item);
      }

      static get observers() {
        return [
          '_itemChanged(item.*)',
        ];
      }

      _itemChanged(newItem) {
        console.log('saved-item', this.savedItem);
        console.log('new-item', newItem);
      }

      _reset() {
        this.set('item', this.savedItem);
      }

    }

    window.customElements.define(MyDemo.is, MyDemo);
  </script>
</dom-module>

Edit

Steps to recreate the problem

  1. Open the demo here.

  2. Open your console.

  3. Navigate in the Plunker to my-demo.html

  4. Click one of the toggle switches.

  5. Notice in the console, the savedItem property updates to the current item property.

  6. Notice, this appears to be the result of the following code block.

      connectedCallback() {
        super.connectedCallback();
        this.set('savedItem', this.item);
      }
    

But how can this be? Because I thought connectedCallback() only fired once at initialization time?


Solution

  • tldr; The connectedCallback() isn't actually being called more than once in this case. savedItem and item are always the same object in your code because JavaScript passes objects by reference.


    Object references

    In the following:

    connectedCallback() {
      this.set('savedItem', this.item);
    }
    
    _reset() {
      this.set('item', this.savedItem);
    }
    

    savedItem and item are both references to the same object. Calling this.set() does not automatically clone the operand (nor does the = operator).

    One solution is to clone the object before assignment (using ES2017 object-spread operator):

    connectedCallback() {
      this.savedItem = {...this.item};
    }
    
    _reset() {
      this.item = {...this.savedItem};
    }
    

    updated plunker


    Best practice (or simpler reset method)

    A simpler way to reset the form is to let iron-form handle the form's reset event, where it resets the form's named inputs to their initial values. This saves you from having to declare savedItem and no extra JavaScript to manage it.

    To accomplish this, wrap the <paper-toggle-button>'s in an <iron-form>, and add name attributes to them. Then, insert an <input type="reset"> in the form, which serves as the reset button.

    <iron-form>
      <form>
        <paper-toggle-button name="alice" checked="{{item.alice}}">Alice</paper-toggle-button>
        <paper-toggle-button name="bob" checked="{{item.bob}}">Bob</paper-toggle-button>
        <paper-toggle-button name="charlie" checked="{{item.charlie}}">Charlie</paper-toggle-button>
        <paper-toggle-button name="dave" checked="{{item.dave}}">Dave</paper-toggle-button>
    
        <input type="reset" class="save">
      </form>
    </iron-form>
    

    demo