diff --git a/src/index.d.ts b/src/index.d.ts index c69419c6..2ade666c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,6 +5,7 @@ declare module render { shallow:boolean; xml:boolean; pretty:boolean; + alwaysRenderedComponents: Array[String]; } function render(vnode:VNode, context?:any, options?:Options):string; diff --git a/src/index.js b/src/index.js index b2de14cd..aebbd4b9 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,13 +48,13 @@ renderToString.render = renderToString; */ let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW); - /** 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 +70,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.indexOf(componentName)===-1) { + nodeName = componentName; } else { let props = getNodeProps(vnode), diff --git a/test/mixedRender.js b/test/mixedRender.js new file mode 100644 index 00000000..24b6f322 --- /dev/null +++ b/test/mixedRender.js @@ -0,0 +1,85 @@ +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}
); + Test.displayName = 'Test'; + + 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}
); + Test.displayName = 'Test'; + + 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}
); + Test.displayName = 'Test'; + + let rendered = render( +
+ asdf +
+ , 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

); + Test.displayName = 'Test'; + Ignored.displayName = 'Ignored'; + + let rendered = render( +
+ +
+ , {}, { shallow: true, alwaysRenderedComponents: ['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 = render( +
+ +
+ , {}, { shallow: true, alwaysRenderedComponents: ['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; + }); +}); 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 }); }); });