From 75b62f0b2c8f9e37959bf8f8b256fcdef44cf97e Mon Sep 17 00:00:00 2001 From: Maira Bello Date: Tue, 30 Aug 2016 17:02:08 -0300 Subject: [PATCH] Improves handling of conditionally rendered elements - Fixes #147 --- .../src/IncrementalDomRenderer.js | 7 +++ packages/metal-jsx/src/JSXRenderer.js | 51 +++++++++++++++++++ packages/metal-jsx/src/iDOMHelpers.js | 3 ++ packages/metal-jsx/test/JSXRenderer.js | 44 ++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/packages/metal-incremental-dom/src/IncrementalDomRenderer.js b/packages/metal-incremental-dom/src/IncrementalDomRenderer.js index 5d414885..c52d5763 100644 --- a/packages/metal-incremental-dom/src/IncrementalDomRenderer.js +++ b/packages/metal-incremental-dom/src/IncrementalDomRenderer.js @@ -423,6 +423,7 @@ class IncrementalDomRenderer extends ComponentRenderer { * @protected */ handleInterceptedCloseCall_(originalFn, tag) { + this.emit(IncrementalDomRenderer.ELEMENT_CLOSED, {tag}); var element = originalFn(tag); this.resetData_(domData.get(element).incDomData_); return element; @@ -464,6 +465,7 @@ class IncrementalDomRenderer extends ComponentRenderer { * @protected */ handleRegularCall_(originalFn, ...args) { + this.emit(IncrementalDomRenderer.ELEMENT_OPENED, {args}); var currComp = IncrementalDomRenderer.getComponentBeingRendered(); var currRenderer = currComp.getRenderer(); if (!currRenderer.rootElementReached_) { @@ -856,6 +858,11 @@ class IncrementalDomRenderer extends ComponentRenderer { var renderingComponents_ = []; var emptyChildren_ = []; +// Constants used as event names. +IncrementalDomRenderer.ELEMENT_OPENED = 'elementOpened'; +IncrementalDomRenderer.ELEMENT_CLOSED = 'elementClosed'; + +// Regex pattern used to find inline listeners. IncrementalDomRenderer.LISTENER_REGEX = /^(?:on([A-Z]\w+))|(?:data-on(\w+))$/; export default IncrementalDomRenderer; diff --git a/packages/metal-jsx/src/JSXRenderer.js b/packages/metal-jsx/src/JSXRenderer.js index 4d547fb0..029bbc2a 100644 --- a/packages/metal-jsx/src/JSXRenderer.js +++ b/packages/metal-jsx/src/JSXRenderer.js @@ -1,11 +1,24 @@ 'use strict'; +import core from 'metal'; import IncrementalDomRenderer from 'metal-incremental-dom'; +const childrenCount = []; + /** * Renderer that handles JSX. */ class JSXRenderer extends IncrementalDomRenderer { + /** + * @inheritDoc + */ + constructor(comp) { + super(comp); + + this.on(IncrementalDomRenderer.ELEMENT_OPENED, this.handleJSXElementOpened_); + this.on(IncrementalDomRenderer.ELEMENT_CLOSED, this.handleJSXElementClosed_); + } + /** * @inheritDoc */ @@ -32,6 +45,32 @@ class JSXRenderer extends IncrementalDomRenderer { } } + /** + * Called when an element is opened during render via incremental dom. Adds + * keys to elements that don't have one yet, according to their position in + * the parent. This helps use cases that use conditionally rendered elements, + * which is very common in JSX. + * @param {!{args: !Array}} data + * @protected + */ + handleJSXElementOpened_({args}) { + if (childrenCount.length > 0) { + const count = ++childrenCount[childrenCount.length - 1]; + if (!core.isDef(args[1])) { + args[1] = JSXRenderer.KEY_PREFIX + count; + } + } + childrenCount.push(0); + } + + /** + * Called when an element is closed during render via incremental dom. + * @protected + */ + handleJSXElementClosed_() { + childrenCount.pop(); + } + /** * @inheritDoc */ @@ -52,6 +91,18 @@ class JSXRenderer extends IncrementalDomRenderer { super.renderIncDom(); } } + + /** + * Skips the current child in the count (used when a conditional render + * decided not to render anything). + */ + static skipChild() { + if (childrenCount.length > 0) { + childrenCount[childrenCount.length - 1]++; + } + } } +JSXRenderer.KEY_PREFIX = '_metal_jsx_'; + export default JSXRenderer; diff --git a/packages/metal-jsx/src/iDOMHelpers.js b/packages/metal-jsx/src/iDOMHelpers.js index 7eb6e474..b9598d0c 100644 --- a/packages/metal-jsx/src/iDOMHelpers.js +++ b/packages/metal-jsx/src/iDOMHelpers.js @@ -1,6 +1,7 @@ 'use strict'; import IncrementalDomRenderer from 'metal-incremental-dom'; +import JSXRenderer from './JSXRenderer'; /** * These helpers are all from "babel-plugin-incremental-dom". See its README @@ -46,6 +47,8 @@ window.iDOMHelpers.renderArbitrary = function(child) { } else { window.iDOMHelpers.forOwn(child, window.iDOMHelpers.renderArbitrary); } + } else if (!child) { + JSXRenderer.skipChild(); } }; diff --git a/packages/metal-jsx/test/JSXRenderer.js b/packages/metal-jsx/test/JSXRenderer.js index 84271e59..fd2f7739 100644 --- a/packages/metal-jsx/test/JSXRenderer.js +++ b/packages/metal-jsx/test/JSXRenderer.js @@ -138,4 +138,48 @@ describe('JSXRenderer', function() { done(); }); }); + + it('should reuse component rendered after a conditionally rendered component', function(done) { + var createdChildren = []; + class ChildComponent extends TestJSXComponent { + constructor(...args) { + super(...args); + createdChildren.push(this); + } + render() { + return Child; + } + } + + class ChildComponent2 extends ChildComponent { + } + + class TestComponent extends TestJSXComponent { + render() { + return
+ {!this.props.hide &&
} +
+
+ } + } + TestComponent.PROPS = { + hide: { + } + } + + component = new TestComponent(); + assert.strictEqual(2, createdChildren.length); + assert.ok(createdChildren[0] instanceof ChildComponent); + assert.ok(createdChildren[1] instanceof ChildComponent2); + assert.ok(!createdChildren[0].isDisposed()); + assert.ok(!createdChildren[1].isDisposed()); + + component.props.hide = true; + component.once('stateSynced', function() { + assert.strictEqual(2, createdChildren.length); + assert.ok(createdChildren[0].isDisposed()); + assert.ok(!createdChildren[1].isDisposed()); + done(); + }); + }); });