diff --git a/browser/DocumentRenderer.js b/browser/DocumentRenderer.js index 66143d5..1a7c11f 100644 --- a/browser/DocumentRenderer.js +++ b/browser/DocumentRenderer.js @@ -145,6 +145,7 @@ class DocumentRenderer { const id = this._getId(element); const localContext = this._localContextProvider.getContextById(id); const hadChildrenNodes = (element.children.length > 0); + let isInitialRender = false; if (!renderingContext) { renderingContext = this._createRenderingContext(); @@ -161,6 +162,7 @@ class DocumentRenderer { const ComponentConstructor = localContext.constructor; if (!instance) { + isInitialRender = true; ComponentConstructor.prototype.$context = this._getComponentContext(localContext, element); instance = new ComponentConstructor(this._locator); instance.$context = ComponentConstructor.prototype.$context; @@ -169,64 +171,73 @@ class DocumentRenderer { this._componentElements[id] = element; - return Promise.resolve() - .then(() => { - // we need to unbind the whole hierarchy only at - // the beginning, not for any new elements - if (!(id in renderingContext.rootIds) || !hadChildrenNodes) { + const shouldComponentUpdateMethod = moduleHelper.getMethodToInvoke(instance, 'shouldComponentUpdate'); + + return moduleHelper.getSafePromise(shouldComponentUpdateMethod) + .then((isShouldComponentUpdate = true) => { + if (!isShouldComponentUpdate && !isInitialRender) { return null; } - return this._unbindAll(element, renderingContext); - }) - .catch((reason) => this._eventBus.emit('error', reason)) - .then(() => this._bindWatcher(localContext, element)) - .then(() => { - const renderMethod = moduleHelper.getMethodToInvoke(instance, 'render'); - return moduleHelper.getSafePromise(renderMethod); - }) - .then((dataContext) => instance.template(dataContext)) - .catch((reason) => this._handleRenderError(element, reason)) - .then(html => { - const isHead = element.tagName === TAG_NAMES.HEAD; + return Promise.resolve() + .then(() => { + // we need to unbind the whole hierarchy only at + // the beginning, not for any new elements + if (!(id in renderingContext.rootIds) || !hadChildrenNodes) { + return null; + } - if (html === '' && isHead) { - return null; - } + return this._unbindAll(element, renderingContext); + }) + .catch((reason) => this._eventBus.emit('error', reason)) + .then(() => this._bindWatcher(localContext, element)) + .then(() => { + const renderMethod = moduleHelper.getMethodToInvoke(instance, 'render'); + return moduleHelper.getSafePromise(renderMethod); + }) + .then((dataContext) => instance.template(dataContext)) + .catch((reason) => this._handleRenderError(element, reason)) + .then(html => { + const isHead = element.tagName === TAG_NAMES.HEAD; + + if (html === '' && isHead) { + return null; + } - const tmpElement = element.cloneNode(false); - tmpElement.innerHTML = html; + const tmpElement = element.cloneNode(false); + tmpElement.innerHTML = html; - this._updateSlotContent(tmpElement); + this._updateSlotContent(tmpElement); - if (isHead) { - this._mergeHead(element, tmpElement); - return null; - } + if (isHead) { + this._mergeHead(element, tmpElement); + return null; + } - morphdom(element, tmpElement, { - onBeforeMorphElChildren: (foundElement) => - foundElement === element || !moduleHelper.isComponentNode(foundElement) - }); + morphdom(element, tmpElement, { + onBeforeMorphElChildren: (foundElement) => + foundElement === element || !moduleHelper.isComponentNode(foundElement) + }); - // Morphing slot elements - let slot = findSlot(element); - let fragment = this._componentSlotContents[id]; + // Morphing slot elements + let slot = findSlot(element); + let fragment = this._componentSlotContents[id]; - if (slot && fragment) { - let content = fragment.cloneNode(true); + if (slot && fragment) { + let content = fragment.cloneNode(true); - Array.from(content.children) - .forEach((child) => { - child.$parentId = this._localContextProvider.getParentById(id); - }); + Array.from(content.children) + .forEach((child) => { + child.$parentId = this._localContextProvider.getParentById(id); + }); - slot.innerHTML = ''; - slot.appendChild(content); - } + slot.innerHTML = ''; + slot.appendChild(content); + } + }) + .then(() => this._bindComponent(element)); }) - .then(() => this._bindComponent(element)) - .catch(reason => this._eventBus.emit('error', reason)); + .catch((reason) => this._eventBus.emit('error', reason)); }; return this._traverseComponentsWithContext([rootElement], action, rootContext) diff --git a/test/browser/DocumentRenderer.js b/test/browser/DocumentRenderer.js index 27e3cc1..5e7a027 100644 --- a/test/browser/DocumentRenderer.js +++ b/test/browser/DocumentRenderer.js @@ -426,6 +426,93 @@ lab.experiment('browser/DocumentRenderer', () => { }); }); + lab.test('Should block render if shouldComponentUpdate return false', (done) => { + const locator = createLocator(); + const eventBus = locator.resolve('eventBus'); + let renderCount = 0; + + const expected = 'test
Hello, World 1!
'; + + eventBus.on('error', done); + jsdom.env({ + html: ' ', + done: function (errors, window) { + class Component { + template () { + renderCount += 1; + return `test
Hello, World ${renderCount}!
`; + } + + shouldComponentUpdate () { + return false; + } + } + + locator.registerInstance('window', window); + + const renderer = new DocumentRenderer(locator); + const element = window.document.createElement('cat-test'); + + renderer.renderComponent(element, { constructor: Component }) + .then(() => { + assert.strictEqual(element.innerHTML, expected); + assert.strictEqual(renderCount, 1); + return renderer.renderComponent(element, { constructor: Component }); + }) + .then(() => { + assert.strictEqual(element.innerHTML, expected); + assert.strictEqual(renderCount, 1); + done(); + }) + .catch(done); + } + }); + }); + + lab.test('Should not block render if shouldComponentUpdate return true', (done) => { + const locator = createLocator(); + const eventBus = locator.resolve('eventBus'); + let renderCount = 0; + + const expected1 = 'test
Hello, World 1!
'; + const expected2 = 'test
Hello, World 2!
'; + + eventBus.on('error', done); + jsdom.env({ + html: ' ', + done: function (errors, window) { + class Component { + template () { + renderCount += 1; + return `test
Hello, World ${renderCount}!
`; + } + + shouldComponentUpdate () { + return true; + } + } + + locator.registerInstance('window', window); + + const renderer = new DocumentRenderer(locator); + const element = window.document.createElement('cat-test'); + + renderer.renderComponent(element, { constructor: Component }) + .then(() => { + assert.strictEqual(element.innerHTML, expected1); + assert.strictEqual(renderCount, 1); + return renderer.renderComponent(element, { constructor: Component }); + }) + .then(() => { + assert.strictEqual(element.innerHTML, expected2); + assert.strictEqual(renderCount, 2); + done(); + }) + .catch(done); + } + }); + }); + lab.test('Should render debug output instead the content when error in debug mode', (done) => { const locator = createLocator(); const eventBus = locator.resolve('eventBus');