atom / etch

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

refs are being assigned to the wrong component

twifty opened this issue · comments

Looking at the following example:

class MainComponent {
  //...
  render () {
    class Component {
      constructor (props, children) {
        this.children = children
        this.props = props

        etch.initialize(this)
      }

      update () {
        return Promise.resolve()
      }

      render () {
        return (<div id={this.props.id}>{ this.children }</div>)
      }
    }

    return (
      <Component id='outer' ref="outer">
        <Component id='middle' ref="middle">
          <div id="inner" ref="inner" />
        </Component>
      </Component>
    )
  }
}

I would expect all ref's to be applied to the MainComponent instance. Instead, I am seeing MainComponent with the nested object refs.outer.refs.middle.refs.inner. Surely this is wrong. If the Component class happened to also ref one of its element, with the name 'middle' or 'inner', all hell would break loose.

Beside the refs being written to the wrong component, the event listeners are also being bound to the wrong instance.

A quick and simple fix is to allow our component constructor to handle the refs. I have done this by wrapping all the children in a proxy, then writing the resulting refs object to the parent. A source change is required for this to work. Within the render function, the options variable needs passing to the constructor as a third parameter. This change should not break BC.

Here is an example of my component using this feature:

class EtchProxy
{
  constructor (_, children, options) {
    this.children = children
    this.options = options

    const wrapper = etch.dom('wrapper', {}, this.children)
    
    this.element = etch.render(wrapper, options)
  }

  getChildren () {
    const children = []

    const inject = (child) => {
      return class {
        constructor () {
          this.element = child
        }
        update () {}
      }
    }

    for (var child = this.element.firstChild; child !== null; child = child.nextSibling) {
      children.push({
        tag: inject(child)
      })
    }

    return children
  }
}

export default class EtchComponent
{
  constructor (properties, children, options) {
    this.properties = properties
    this.options = options

    if (etch.getScheduler() !== atom.views) {
      etch.setScheduler(atom.views);
    }

    // Proxy and render the children
    this.proxy = etch.dom(EtchProxy, {}, ...children)
    etch.render(this.proxy, options)

    // this.proxy and options.refs now contains the childrens refs. We now need to replace the
    // children with their pre-rendered equivlents.
    this.children = this.proxy.component.getChildren()

    etch.initialize(this)
  }
}

Reproducible with npm test and this patch:

diff --git a/test/unit/initialize.test.js b/test/unit/initialize.test.js
index 09bfd18..c794732 100644
--- a/test/unit/initialize.test.js
+++ b/test/unit/initialize.test.js
@@ -54,6 +54,45 @@ describe('etch.initialize(component)', () => {
     expect(component.refs.selected.textContent).to.equal('one')
   })
 
+  it('nests references correctly', async () => {
+    class Component {
+      constructor(props, children) {
+        this.children = children
+        etch.initialize(this)
+      }
+
+      update() {}
+
+      render () {
+        return <div>{this.children}</div>
+      }
+    }
+
+    class TestHarness {
+      constructor() {
+        etch.initialize(this)
+      }
+
+      update() {}
+
+      render() {
+        return (
+          <Component ref="outer">
+            <Component ref="middle">
+              <div ref="inner" />
+            </Component>
+          </Component>
+        )
+      }
+    }
+
+    const harness = new TestHarness()
+    expect(harness.refs.outer).to.be.ok
+    expect(harness.refs.middle).to.be.ok
+    expect(harness.refs.inner).to.be.ok
+    expect(harness.refs.outer.refs.middle).to.be.undefined
+  })
+
   it('throws an exception if undefined is returned from render', () => {
     let component = {
       render () {},

/cc @nathansobo for thoughts on this / the event listeners — there's a discrepancy between who renders the child component (the component just above it) vs who owns the component (the component that included the ref="..." properties).

Taking a look this morning.

The current approach always binds refs to the nearest ancestor component in the tree. That doesn't seem unreasonable to me. Can someone please articulate why it is?

@nathansobo Lets say I have a tree view consisting of 3 components, the View outer container, the Branch collapsible <ul>, and the Leaf a <li>.

When I create the tree, I would also like to add mouse hooks, be able to hide/show elements and be able to add a '.selected' class to individual elements. I would add a 'ref' property and an 'onClick' handler. With the current approach of binding refs to the nearest ancestor, those refs are no longer visible to the outer most class (not without accessing them through other refs.name.refs.actual) AND the onClick function is bound to the nearest ancestor.

If the nearest ancestor has a ref with the same name declared in the outermost class, you have a name collision. Remember, the end user will probably not know what ref names I have used within my components, they will only be visible after he/she looks into the components source code file. The second problem would be the event handlers. They essentially need to be wrapped in functions containing the good old const self = this to ensure they don't try calling a method, which may or may not exist, on the nearest component.

In short, if I declare refs and event handlers in the outer class, they should be bound to the outer class. Components within the children array, should be treated as a block of raw HTML and rendered as such.

Just to be clear, I'm not saying ALL refs need to be propagated up to the root, but only the refs present in Component constructors children array.

Closed via #72