From 9c1b31e7f8bda85a06aaaea18b8b6462f67ea645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20S=C3=A6grov?= Date: Thu, 21 Sep 2017 16:57:37 +0200 Subject: [PATCH 1/4] Add support for mixed rendering Mixed rendering is a mode that works like shallow rendering, but where you can specify a list of components you want to render even tho they are deeply nested. --- src/index.d.ts | 2 ++ src/index.js | 25 ++++++++++++-- test/mixedRender.js | 84 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 test/mixedRender.js diff --git a/src/index.d.ts b/src/index.d.ts index c69419c6..0bc7e113 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,10 +5,12 @@ declare module render { shallow:boolean; xml:boolean; pretty:boolean; + alwaysRenderedComponents: Array[String]; } function render(vnode:VNode, context?:any, options?:Options):string; function shallowRender(vnode:VNode, context?:any):string; + function mixedRender(vnode: VNode, alwaysRenderedComponents: Array[String], context?: any): string; } export = render; diff --git a/src/index.js b/src/index.js index b2de14cd..14800590 100644 --- a/src/index.js +++ b/src/index.js @@ -48,12 +48,27 @@ renderToString.render = renderToString; let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW); +/** + * Only render elements, leaving Components inline as `` + * except those specified in fullyRenderedComponents. + * This method is just a convenience alias for `render(vnode, context, { shallow:true, alwaysRenderedComponented: [] })` + * @param {VNode} vnode JSX VNode to render. + * @param {Array} alwaysRenderedComponents List of components that should be rendered with shallow rendering + * @param {Object} [context={}] Optionally pass an initial context object through the render path. + */ +let mixedRender = (vnode, alwaysRenderedComponents = [], context) => { + const opts = Object.assign({ alwaysRenderedComponents }, SHALLOW); + return renderToString(vnode, context, opts); +}; + + /** The default export is an alias of `render()`. */ export default function renderToString(vnode, context, opts, inner, isSvgMode) { let { nodeName, attributes, children } = vnode || EMPTY, isComponent = false; context = context || {}; opts = opts || {}; + opts.alwaysRenderedComponents = opts.alwaysRenderedComponents || []; let pretty = opts.pretty, indentChar = typeof pretty==='string' ? pretty : '\t'; @@ -69,9 +84,12 @@ export default function renderToString(vnode, context, opts, inner, isSvgMode) { // components if (typeof nodeName==='function') { + let componentName = getComponentName(nodeName); isComponent = true; - if (opts.shallow && (inner || opts.renderRootComponent===false)) { - nodeName = getComponentName(nodeName); + if (opts.shallow && + (inner || opts.renderRootComponent===false) && + !opts.alwaysRenderedComponents.includes(componentName)) { + nodeName = componentName; } else { let props = getNodeProps(vnode), @@ -235,5 +253,6 @@ renderToString.shallowRender = shallowRender; export { renderToString as render, renderToString, - shallowRender + shallowRender, + mixedRender }; diff --git a/test/mixedRender.js b/test/mixedRender.js new file mode 100644 index 00000000..e2d543fd --- /dev/null +++ b/test/mixedRender.js @@ -0,0 +1,84 @@ +import { mixedRender } from '../src'; +import { h, Component } from 'preact'; +import chai, { expect } from 'chai'; +import { spy, match } from 'sinon'; +import sinonChai from 'sinon-chai'; +chai.use(sinonChai); + +describe('mixedRender()', () => { + it('should not render nested components when not white listed', () => { + let Test = spy( ({ foo, children }) =>
test child{ children }
); + Test.displayName = 'Test'; + + let rendered = mixedRender( +
+ asdf +
+ ); + + expect(rendered).to.equal(`
asdf
`); + expect(Test).not.to.have.been.called; + }); + + it('should always render root component', () => { + let Test = spy( ({ foo, children }) =>
test child{ children }
); + Test.displayName = 'Test'; + + let rendered = mixedRender( + + asdf + + ); + + expect(rendered).to.equal(`
test childasdf
`); + expect(Test).to.have.been.calledOnce; + }); + + it('should render nested components when they are white listed', () => { + let Test = spy( ({ foo, children }) =>
test child{ children }
); + Test.displayName = 'Test'; + + let rendered = mixedRender( +
+ asdf +
+ , ['Test']); + + expect(rendered).to.equal(`
test childasdf
`); + expect(Test).to.have.been.called; + }); + + it('should not render nested components inside a whitelisted component', () => { + let Test = spy( ({ foo, children }) =>
test child{ children }
); + let Ignored = spy( ({ title, children }) =>

This {title} should not be rendered

); + Test.displayName = 'Test'; + Ignored.displayName = 'Ignored'; + + let rendered = mixedRender( +
+ +
+ , ['Test']); + + expect(rendered).to.equal(`
test child
`); + expect(Test).to.have.been.called; + expect(Ignored).to.not.have.been.called; + }); + + it('should render deeply nested components when all are white listed', () => { + let Test = spy( ({ foo, children }) =>
test child{ children }
); + let Ignored = spy( ({ title, children }) =>

This {title} should be rendered

); + Test.displayName = 'Test'; + Ignored.displayName = 'Ignored'; + + let rendered = mixedRender( +
+ +
+ , ['Test', 'Ignored']); + + expect(rendered).to.equal(`
test child

This FooBarTitle should be rendered

`); + expect(Test).to.have.been.called; + expect(Ignored).to.have.been.called; + }); +}); From 8514400d09cb18730420a58891ec299272e2d3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20S=C3=A6grov?= Date: Fri, 22 Sep 2017 11:39:39 +0200 Subject: [PATCH 2/4] Export mixedRender function --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index 14800590..090dd101 100644 --- a/src/index.js +++ b/src/index.js @@ -248,6 +248,7 @@ function getFallbackComponentName(component) { return name; } renderToString.shallowRender = shallowRender; +renderToString.mixedRender = mixedRender; export { From 5d9652a0cae1a4998ebfd2ad3edb56b0e130e2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20S=C3=A6grov?= Date: Tue, 17 Oct 2017 09:21:00 +0200 Subject: [PATCH 3/4] Remove mixedRender as an exported function --- src/index.d.ts | 1 - src/index.js | 20 ++------------------ test/mixedRender.js | 39 ++++++++++++++++++++------------------- test/render.js | 10 +++++----- 4 files changed, 27 insertions(+), 43 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 0bc7e113..2ade666c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -10,7 +10,6 @@ declare module render { function render(vnode:VNode, context?:any, options?:Options):string; function shallowRender(vnode:VNode, context?:any):string; - function mixedRender(vnode: VNode, alwaysRenderedComponents: Array[String], context?: any): string; } export = render; diff --git a/src/index.js b/src/index.js index 090dd101..77895bc7 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,7 @@ const VOID_ELEMENTS = [ * @param {Boolean} [options.shallow=false] If `true`, renders nested Components as HTML elements (``). * @param {Boolean} [options.xml=false] If `true`, uses self-closing tags for elements without children. * @param {Boolean} [options.pretty=false] If `true`, adds whitespace for readability + * @param {string[]} [options.alwaysRenderedComponents=[]] List of components that should be rendered with shallow rendering */ renderToString.render = renderToString; @@ -47,21 +48,6 @@ renderToString.render = renderToString; */ let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW); - -/** - * Only render elements, leaving Components inline as `` - * except those specified in fullyRenderedComponents. - * This method is just a convenience alias for `render(vnode, context, { shallow:true, alwaysRenderedComponented: [] })` - * @param {VNode} vnode JSX VNode to render. - * @param {Array} alwaysRenderedComponents List of components that should be rendered with shallow rendering - * @param {Object} [context={}] Optionally pass an initial context object through the render path. - */ -let mixedRender = (vnode, alwaysRenderedComponents = [], context) => { - const opts = Object.assign({ alwaysRenderedComponents }, SHALLOW); - return renderToString(vnode, context, opts); -}; - - /** The default export is an alias of `render()`. */ export default function renderToString(vnode, context, opts, inner, isSvgMode) { let { nodeName, attributes, children } = vnode || EMPTY, @@ -248,12 +234,10 @@ function getFallbackComponentName(component) { return name; } renderToString.shallowRender = shallowRender; -renderToString.mixedRender = mixedRender; export { renderToString as render, renderToString, - shallowRender, - mixedRender + shallowRender }; diff --git a/test/mixedRender.js b/test/mixedRender.js index e2d543fd..24b6f322 100644 --- a/test/mixedRender.js +++ b/test/mixedRender.js @@ -1,64 +1,65 @@ -import { mixedRender } from '../src'; -import { h, Component } from 'preact'; +import { render } from '../src'; +import {h, Component} from 'preact'; import chai, { expect } from 'chai'; import { spy, match } from 'sinon'; import sinonChai from 'sinon-chai'; + chai.use(sinonChai); describe('mixedRender()', () => { it('should not render nested components when not white listed', () => { - let Test = spy( ({ foo, children }) =>
test child{ children }
); + let Test = spy(({ foo, children }) =>
test child{children}
); Test.displayName = 'Test'; - let rendered = mixedRender( + let rendered = render(
asdf
- ); + , {}, { shallow: true, alwaysRenderedComponents: [] }); expect(rendered).to.equal(`
asdf
`); expect(Test).not.to.have.been.called; }); it('should always render root component', () => { - let Test = spy( ({ foo, children }) =>
test child{ children }
); + let Test = spy(({ foo, children }) =>
test child{children}
); Test.displayName = 'Test'; - let rendered = mixedRender( + let rendered = render( asdf - ); + , {}, { shallow: true, alwaysRenderedComponents: [] }); expect(rendered).to.equal(`
test childasdf
`); expect(Test).to.have.been.calledOnce; }); it('should render nested components when they are white listed', () => { - let Test = spy( ({ foo, children }) =>
test child{ children }
); + let Test = spy(({ foo, children }) =>
test child{children}
); Test.displayName = 'Test'; - let rendered = mixedRender( + let rendered = render(
asdf
- , ['Test']); + , undefined, { alwaysRenderedComponents: ['Test'] }); expect(rendered).to.equal(`
test childasdf
`); expect(Test).to.have.been.called; }); it('should not render nested components inside a whitelisted component', () => { - let Test = spy( ({ foo, children }) =>
test child{ children }
); - let Ignored = spy( ({ title, children }) =>

This {title} should not be rendered

); + let Test = spy(({ foo, children }) =>
test child{children}
); + let Ignored = spy(({ title, children }) =>

This {title} should not be rendered

); Test.displayName = 'Test'; Ignored.displayName = 'Ignored'; - let rendered = mixedRender( + let rendered = render(
- , ['Test']); + , {}, { shallow: true, alwaysRenderedComponents: ['Test'] }); expect(rendered).to.equal(`
test child
`); expect(Test).to.have.been.called; @@ -66,16 +67,16 @@ describe('mixedRender()', () => { }); it('should render deeply nested components when all are white listed', () => { - let Test = spy( ({ foo, children }) =>
test child{ children }
); - let Ignored = spy( ({ title, children }) =>

This {title} should be rendered

); + let Test = spy(({ foo, children }) =>
test child{children}
); + let Ignored = spy(({ title, children }) =>

This {title} should be rendered

); Test.displayName = 'Test'; Ignored.displayName = 'Ignored'; - let rendered = mixedRender( + let rendered = render(
- , ['Test', 'Ignored']); + , {}, { shallow: true, alwaysRenderedComponents: ['Test', 'Ignored'] }); expect(rendered).to.equal(`
test child

This FooBarTitle should be rendered

`); expect(Test).to.have.been.called; diff --git a/test/render.js b/test/render.js index 2fa9c5ed..bfc1e878 100644 --- a/test/render.js +++ b/test/render.js @@ -500,7 +500,7 @@ describe('render', () => { expect(renderXml(
)).to.equal(`
`); }); }); - + describe('state locking', () => { it('should set _disable and __x to true', () => { let inst; @@ -513,9 +513,9 @@ describe('render', () => { return
; } } - + expect(render()).to.equal('
'); - + expect(inst).to.have.property('_disable', true); expect(inst).to.have.property('__x', true); }); @@ -533,9 +533,9 @@ describe('render', () => { return ; } } - + expect(render()).to.equal('
'); - + expect(Bar).to.have.been.calledOnce.and.calledWithMatch({ count: 1 }); }); }); From 48a2c9b19d5f7f63af6f38f324204bcc7e2f169c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20S=C3=A6grov?= Date: Tue, 17 Oct 2017 10:38:37 +0200 Subject: [PATCH 4/4] Replace includes with indexOf --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 77895bc7..aebbd4b9 100644 --- a/src/index.js +++ b/src/index.js @@ -74,7 +74,7 @@ export default function renderToString(vnode, context, opts, inner, isSvgMode) { isComponent = true; if (opts.shallow && (inner || opts.renderRootComponent===false) && - !opts.alwaysRenderedComponents.includes(componentName)) { + opts.alwaysRenderedComponents.indexOf(componentName)===-1) { nodeName = componentName; } else {