atom / etch

Builds components using a simple and explicit API around virtual-dom

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Components without an update method throw an error when updated

mikedeltalima opened this issue · comments

The readme claims that any class that constructs an object with an .element property can be used as a component, but component-widget.js attempts to call update in line 63 without checking if it is defined, despite a comment above the offending function that seems to agree with the readme.

Looks like this was changed in #15 and some comments/docs haven't been updated. Does that sound correct to you @nathansobo? What's the thoughts nowadays for "components" that aren't built with Etch (and just have a constructor and a .element)?

Your component needs to implement update, but can still be a simple object. We found it to be too surprising to be able to re-render the child component from some parent JSX with different properties and not have any way of relaying those properties to it. You can still implement the child component with any technology you want so long as you support that interface. I'll update the README.

Could you update the comment in component-widget.js, too? Also, if you implement update, don't you have to implement render as well, and call etch.update(this) in order to create a new component with your element? I'm having trouble getting it to work, actually.

EDIT: Okay, so I debugged a bit more, and I think the problem is that virtual-dom expects a widget's update function to return the new widget, and the one in component-widget.js doesn't return anything.

@mikedeltalima all you should have to do is define an update method. It doesn't have to do anything if you don't want. This should be sufficient:

class MyCustomChildComponent
  constructor (props) {
    this.element = this.buildElementHoweverYouWant()
  }

  update (props) {
    // mutate the element if you want
  }
}

Then in parent JSX:

<div>
  <MyCustomChildComponent foo="bar" />
</div>

@nathansobo: I tried that, but then when the element exists and I need to update the data, it doesn't update. Mutating the element where you put the comment in the above example does not actually change the displayed element, because virtual-dom expects the new element to be returned by the widget's update function in the widgetPatch function in patch-op.js.

One way to fix it is to return the new element at the end of the update function in component-widget.js. Did you bypass the patch-op.js functionality because of refs? (Actually that's not a very informed guess, but I don't want to sound confrontational by asking "why did you..." when I'm just genuinely curious)

@mikedeltalima it sounds like you're just inserting an object directly in the JSX instead of using a constructor? Am I right about that?

@nathansobo actually, no. My constructor looks a lot like your example

Okay, I'm going to investigate this and write a test of the functionality. Sorry for the hassle.

No problem at all. I'm grateful for your help. Here is where component-widget.js always returns undefined and therefore this condition is always false.

Check out the test I added in 8505ffb. This is the usage pattern I had in mind. Notice how the ChildComponent makes no reference to Etch and uses manual DOM manipulation. Could you create a simple test case that demonstrates what you're trying to do that isn't working? You could even just paste it here as a comment.

In your test, if you replace everything from where ChildComponent is defined with the following, you can see that the node will not be replaced.

    class ChildComponent {
      constructor ({greeting}) {
        this.element = document.createElement('div')
        this.element.textContent = greeting
      }

      update ({greeting}) {
        this.element = document.createElement('table')
      }
    }

    let component = new ParentComponent({greeting: 'Hello'})
    expect(component.element.tagName).to.equal('DIV')

    component.update({greeting: 'Goodbye'})
    expect(component.element.tagName).to.equal('TABLE')

Yeah, but that's more of an issue with your child component's implementation. Etch only calls update on your component. It's up to you to replace the element in place via DOM APIs if that's needed. I suppose we could extend Etch to check if the child component's element changes, but that's not complexity that I'm sure would be worth adding right now. How about...

this.element.parentNode.replaceChild(this.element, newElement)

... for the time being.

/cc @BinaryMuse do you think Etch should detect if a non-Etch-based child component's element changes and replace it automatically?

@nathansobo I think if Etch takes care of mutating the node's contents, it should also take care of mutating the node. Anyway, your suggestion fails in the above test with the message "Error: Failed to execute 'replaceChild' on 'Node': The node to be replaced is not a child of this node"

Is there a reason why we can't just return the node so that virtual-dom handles the change?

@mikedeltalima Good point about returning the element in the virtual-dom widget. I overlooked that feature. Let me know if eb0293d addresses your issue.

Okay, publish 0.6.1, which I think should work better. By the way in my example that didn't work, I think had the arguments switched. Now you shouldn't need to do that anyway. Thanks for your time.

I'll confirm later, but looking at the code I think it does. Thanks :)

EDIT: Yes, it works now. Thanks again