4

I have a LitElement that represents a file upload for multiple files. This uses a sub-component that represents each file.

I'm struggling to find examples of the best practice for propagating changes into the sub component using LitElements as it appears to be very different from Polymer 3

Here's a cut down example of what I'm trying:

import './uploadFile.js';
class Upload extends LitElement {
  ...
  static get properties() { return { files: Object } }
  _render({files}) {
    return html`
      <input type="file" multiple onchange="...">
      ${this.renderFiles(files)}`
  }
  renderFiles(files) {
    const filesTemplate = [];                                                                                        
    for (var i = 0; i < files.length; i++) {                                                                         
      filesTemplate.push(html`
        <upload-file file="${files[i]}"></upload-file>
       `);                                
    }                                                                                                                
    return filesTemplate;                                                                                            
  }
}

When I update the status of a file the upload component re-renders but the upload-file component does not.

What am I doing wrong here? There aren't may examples of LitElement usage out there.

TIA

DaveB
  • 190
  • 1
  • 7

1 Answers1

14

Best practice is "properties down, events up"; meaning that parent elements should share data with children by binding properties to them, and child elements should share data with parents by raising an event with relevant data in the detail of the event.

I can't comment on what you're doing wrong as I can't see how you're updating the status of the files, or your implementation of the child element.

One thing to be aware of is that because of dirty checking, lit-element can only observe changes to the top-level properties that you've listed in the properties getter, and not their sub-properties.

Something like

this.myObj = Object.assign({}, this.myObj, {thing: 'stuff'});

will trigger changes to an object and its sub-properties to render, while

this.myObj.thing='stuff';

will not.

To get sub-property changes to trigger a re-render, you would need to either request one with requestRender() or clone the whole object.

Here is some sample code showing a basic "properties down, events up" model:

Warning: lit-element is still pre-release and syntax will change.

parent-element.js

import { LitElement, html} from '@polymer/lit-element';
import './child-element.js';

class ParentElement extends LitElement {
  static get properties(){
    return {
      myArray: Array
    };
  }
  constructor(){
    super();
    this.myArray = [ 
      { val: 0, done: false },
      { val: 1, done: false },
      { val: 2, done: false },
      { val: 3, done: false }
    ];
  }
  _render({myArray}){
    return html`
      ${myArray.map((i, index) => { 
        return html`
          <child-element 
            on-did-thing="${(e) => this.childDidThing(index, i.val)}" 
            val="${i.val}"
            done="${i.done}">
          </child-element>
      `})}
    `;
  }

  childDidThing(index, val){
    this.myArray[index].done=true;
    /**
     * Mutating a complex property (i.e changing one of its items or
     * sub-properties) does not trigger a re-render, so we must
     * request one:
     */
    this.requestRender();

    /**
     * Alternative way to update a complex property and make
     * sure lit-element observes the change is to make sure you 
     * never mutate (change sub-properties of) arrays and objects.
     * Instead, rewrite the whole property using Object.assign.
     * 
     * For an array, this could be (using ES6 object syntax):
     * 
     * this.myArray = 
     * Object.assign([], [...this.myArray], { 
     *   [index]: { val: val, done: true }
     * });
     * 
    */
  }
}
customElements.define('parent-element', ParentElement);

child-element.js

import { LitElement, html} from '@polymer/lit-element';

class ChildElement extends LitElement {
  static get properties(){
    return {
      val: Number,
      done: Boolean
    };
  }
  _render({val, done}){
    return html`
      <div>
        Value: ${val} Done: ${done} 
        <button on-click="${(e) => this.didThing(e)}">do thing</button>
      </div>
    `;
  }
  didThing(e){
    var event = new CustomEvent('did-thing', { detail: { stuff: 'stuff'} });
    this.dispatchEvent(event);
  }
}
customElements.define('child-element', ChildElement);

Hope that helps.

Kate Jeffreys
  • 559
  • 2
  • 7
  • Thanks @kate, that's a useful example. The data is updated by async call. The issue I'm having is that the render Upload is being called just fine but it does **not** call render in the UploadFile child component. Is there something wrong with the way I'm calling renderFiles I wonder? – DaveB Sep 13 '18 at 09:30
  • Is there any way to inspect how the data is bound with in the LitElements? – DaveB Sep 13 '18 at 09:34
  • I have rewritten the code to not use a sub component and it is rendering as expected now. Wonder if there's some odd behaviour in the boundary between components. – DaveB Sep 13 '18 at 12:41
  • @DaveB Glad you got it working, sorry I wasn't able to look at your other questions yesterday. It would still be great to see your implementation of the sub component so I can help with raising any lit-element bugs and/or see what you did wrong. My instinct is that the failure to rerender the subcomponent is to do with observability of object subproperty changes, but maybe that's just because it has messed me up so many times :D If you post more code I will take a look, and/or you can raise an issue on the github repo https://github.com/Polymer/lit-element/ if you found a bug. – Kate Jeffreys Sep 14 '18 at 20:12