Skip to content

Commit 8803718

Browse files
committed
Rough merge for 2.x:
- Update preact and add it as a peer dep - Test for 3.3.0 + shallow + `dangerouslySetInnerHTML` - Fixes for 3.0.0, add support for shallow rendering, and `dangerouslySetInnerHTML`.
2 parents aa664a1 + 2ff2aa0 commit 8803718

File tree

5 files changed

+342
-228
lines changed

5 files changed

+342
-228
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@
2626
"url": "https://github.com/developit/preact-render-to-string/issues"
2727
},
2828
"homepage": "https://github.com/developit/preact-render-to-string",
29+
"peerDependencies": {
30+
"preact": "*"
31+
},
2932
"devDependencies": {
3033
"babel": "^5.8.23",
3134
"babel-eslint": "^4.1.3",
3235
"chai": "^3.3.0",
3336
"eslint": "^1.7.1",
3437
"mocha": "^2.3.3",
35-
"preact": "^1.5.0",
38+
"preact": "^3.3.0",
3639
"sinon": "^1.17.1",
3740
"sinon-chai": "^2.8.0"
3841
}

src/index.js

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const NO_RENDER = { render: false };
1+
2+
const SHALLOW = { shallow: true };
23

34
const ESC = {
45
'<': '&lt;',
@@ -7,30 +8,47 @@ const ESC = {
78
'&': '&amp;'
89
};
910

10-
const EMPTY = {};
11-
1211
const HOP = Object.prototype.hasOwnProperty;
1312

14-
let encodeEntities = s => String(s).replace(/[<>"&]/g, a => ESC[a] || a);
13+
let encodeEntities = s => String(s).replace(/[<>"&]/g, escapeChar);
14+
15+
let escapeChar = a => ESC[a] || a;
1516

16-
/** Convert JSX to a string, rendering out all nested components along the way.
17-
* @param {VNode} vnode A VNode, generally created via JSX
18-
* @param {Object} [options] Options for the renderer
19-
* @param {Boolean} [options.shallow=false] Passing `true` stops at Component VNodes without rendering them. Note: a component located at the root will always be rendered.
17+
18+
/** Render Preact JSX + Components to an HTML string.
19+
* @name render
20+
* @function
21+
* @param {VNode} vnode JSX VNode to render.
22+
* @param {Object} [options={}] Rendering options
23+
* @param {Boolean} [options.shallow=false] If `true`, renders nested Components as HTML elements (`<Foo a="b" />`).
24+
* @param {Boolean} [options.xml=false] If `true`, uses self-closing tags for elements without children.
25+
* @param {Object} [context={}] Optionally pass an initial context object through the render path.
2026
*/
21-
export function render(vnode, opts) {
22-
return internalRender(vnode, opts || EMPTY, true);
23-
}
27+
renderToString.render = renderToString;
2428

25-
export function shallowRender(vnode, opts) {
26-
return internalRender(vnode, { shallow:true, ...(opts || EMPTY) }, true);
27-
}
2829

29-
export default render;
30+
/** Only render elements, leaving Components inline as `<ComponentName ... />`.
31+
* This method is just a convenience alias for `render(vnode, context, { shallow:true })`
32+
* @name shallow
33+
* @function
34+
* @param {VNode} vnode JSX VNode to render.
35+
* @param {Object} [context={}] Optionally pass an initial context object through the render path.
36+
*/
37+
renderToString.shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW);
38+
3039

40+
/** You can actually skip preact entirely and import this empty Component base class (or not use a base class at all).
41+
* preact-render-to-string doesn't use any of Preact's functionality to do its job.
42+
* @name Component
43+
* @class
44+
*/
45+
// renderToString.Component = function Component(){};
3146

32-
function internalRender(vnode, opts, root) {
47+
48+
/** The default export is an alias of `render()`. */
49+
export default function renderToString(vnode, context, opts, inner) {
3350
let { nodeName, attributes, children } = vnode || EMPTY;
51+
context = context || {};
3452

3553
// #text nodes
3654
if (!nodeName) {
@@ -39,7 +57,7 @@ function internalRender(vnode, opts, root) {
3957

4058
// components
4159
if (typeof nodeName==='function') {
42-
if (opts.shallow===true && !root) {
60+
if (opts && opts.shallow && inner) {
4361
nodeName = getComponentName(nodeName);
4462
}
4563
else {
@@ -48,40 +66,63 @@ function internalRender(vnode, opts, root) {
4866

4967
if (typeof nodeName.prototype.render!=='function') {
5068
// stateless functional components
51-
rendered = nodeName(props);
69+
rendered = nodeName(props, context);
5270
}
5371
else {
5472
// class-based components
55-
let c = new nodeName(props);
56-
c.setProps(props, NO_RENDER);
57-
rendered = c.render(c.props = props, c.state);
73+
let c = new nodeName(props, context);
74+
c.props = props;
75+
c.context = context;
76+
rendered = c.render(c.props, c.state, c.context);
77+
78+
if (c.getChildContext) {
79+
context = c.getChildContext();
80+
}
5881
}
5982

60-
return internalRender(rendered, opts, false);
83+
return renderToString(rendered, context, opts, !opts || opts.shallowHighOrder!==false);
6184
}
6285
}
6386

6487
// render JSX to HTML
65-
let s = `<${nodeName}`;
88+
let s = `<${nodeName}`,
89+
html;
90+
6691
for (let name in attributes) {
6792
if (HOP.call(attributes, name)) {
6893
let v = attributes[name];
6994
if (name==='className') {
7095
if (attributes['class']) continue;
7196
name = 'class';
7297
}
73-
if (v!==null && v!==undefined && typeof v!=='function') {
74-
s += ` ${name}="${encodeEntities(String(v))}"`;
98+
if (name==='dangerouslySetInnerHTML') {
99+
html = v && v.__html;
100+
}
101+
else if (v!==null && v!==undefined && typeof v!=='function') {
102+
s += ` ${name}="${encodeEntities(v)}"`;
75103
}
76104
}
77105
}
78106
s += '>';
79-
if (children && children.length) {
80-
s += children.map( child => internalRender(child, opts, false) ).join('');
107+
108+
if (html) {
109+
s += html;
110+
}
111+
else {
112+
let len = children && children.length;
113+
if (len) {
114+
for (let i=0; i<len; i++) {
115+
s += renderToString(children[i], context, opts, true);
116+
}
117+
}
118+
else if (opts && opts.xml) {
119+
return s.substring(0, s.length-1) + ' />';
120+
}
81121
}
122+
82123
s += `</${nodeName}>`
83124
return s;
84-
}
125+
};
85126

86127
function getComponentName(component) {
87128
return component.displayName || component.name || component.prototype.displayName || component.prototype.name || (Function.prototype.toString.call(component).match(/\s([^\(]+)/) || EMPTY)[1] || 'Component';

test/index.js

Lines changed: 9 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -1,209 +1,19 @@
1-
import { default as defaultRender, render, shallowRender } from '../src';
2-
import { h, Component } from 'preact';
3-
import { expect, use } from 'chai';
4-
import { spy, match } from 'sinon';
5-
import sinonChai from 'sinon-chai';
6-
use(sinonChai);
1+
import renderToString, { render, shallowRender } from '../src';
2+
import { expect } from 'chai';
73

84
describe('render-to-string', () => {
9-
describe('default()', () => {
10-
it('should be render()', () => {
11-
expect(defaultRender).to.equal(render);
5+
describe('exports', () => {
6+
it('exposes renderToString as default', () => {
7+
expect(renderToString).to.be.a('function');
128
});
13-
});
149

15-
describe('render()', () => {
16-
it('should be a function', () => {
10+
it('exposes render as a named export', () => {
1711
expect(render).to.be.a('function');
12+
expect(render).to.equal(renderToString);
1813
});
1914

20-
describe('Basic JSX', () => {
21-
it('should render JSX', () => {
22-
let rendered = render(<div class="foo">bar</div>),
23-
expected = `<div class="foo">bar</div>`;
24-
25-
expect(rendered).to.equal(expected);
26-
});
27-
28-
it('should omit null and undefined attributes', () => {
29-
let rendered = render(<div a={null} b={undefined} />),
30-
expected = `<div></div>`;
31-
32-
expect(rendered).to.equal(expected);
33-
});
34-
35-
it('should omit functions', () => {
36-
let rendered = render(<div a={()=>{}} b={function(){}} />),
37-
expected = `<div></div>`;
38-
39-
expect(rendered).to.equal(expected);
40-
});
41-
42-
it('should encode entities', () => {
43-
let rendered = render(<div a={'"<>&'}>{'"<>&'}</div>),
44-
expected = `<div a="&quot;&lt;&gt;&amp;">&quot;&lt;&gt;&amp;</div>`;
45-
46-
expect(rendered).to.equal(expected);
47-
});
48-
});
49-
50-
describe('Functional Components', () => {
51-
it('should render functional components', () => {
52-
let Test = spy( ({ foo, children }) => <div foo={foo}>{ children }</div> );
53-
54-
let rendered = render(<Test foo="test">content</Test>);
55-
56-
expect(rendered)
57-
.to.equal(`<div foo="test">content</div>`);
58-
59-
expect(Test)
60-
.to.have.been.calledOnce
61-
.and.calledWithExactly(
62-
match({
63-
foo: 'test',
64-
children: ['content']
65-
})
66-
);
67-
});
68-
69-
it('should render functional components within JSX', () => {
70-
let Test = spy( ({ foo, children }) => <div foo={foo}>{ children }</div> );
71-
72-
let rendered = render(
73-
<section>
74-
<Test foo={1}><span>asdf</span></Test>
75-
</section>
76-
);
77-
78-
expect(rendered)
79-
.to.equal(`<section><div foo="1"><span>asdf</span></div></section>`);
80-
81-
expect(Test)
82-
.to.have.been.calledOnce
83-
.and.calledWithExactly(
84-
match({
85-
foo: 1,
86-
children: [
87-
match({ nodeName:'span', children:['asdf'] })
88-
]
89-
})
90-
);
91-
});
92-
});
93-
94-
describe('Classical Components', () => {
95-
it('should render classical components', () => {
96-
class Test extends Component {
97-
render({ foo, children }, state) {
98-
return <div foo={foo}>{ children }</div>;
99-
}
100-
}
101-
102-
Test = spy(Test);
103-
spy(Test.prototype, 'render');
104-
105-
let rendered = render(<Test foo="test">content</Test>);
106-
107-
const PROPS = {
108-
foo: 'test',
109-
children: ['content']
110-
};
111-
112-
expect(rendered)
113-
.to.equal(`<div foo="test">content</div>`);
114-
115-
expect(Test)
116-
.to.have.been.calledOnce
117-
.and.calledWith(match(PROPS));
118-
119-
expect(Test.prototype.render)
120-
.to.have.been.calledOnce
121-
.and.calledWithExactly(
122-
match(PROPS),
123-
match({}) // empty state
124-
);
125-
});
126-
127-
it('should render classical components within JSX', () => {
128-
class Test extends Component {
129-
render({ foo, children }, state) {
130-
return <div foo={foo}>{ children }</div>;
131-
}
132-
}
133-
134-
Test = spy(Test);
135-
spy(Test.prototype, 'render');
136-
137-
let rendered = render(
138-
<section>
139-
<Test foo={1}><span>asdf</span></Test>
140-
</section>
141-
);
142-
143-
expect(rendered)
144-
.to.equal(`<section><div foo="1"><span>asdf</span></div></section>`);
145-
146-
expect(Test).to.have.been.calledOnce;
147-
148-
expect(Test.prototype.render)
149-
.to.have.been.calledOnce
150-
.and.calledWithExactly(
151-
match({
152-
foo: 1,
153-
children: [
154-
match({ nodeName:'span', children:['asdf'] })
155-
]
156-
}),
157-
match({}) // empty state
158-
);
159-
});
160-
});
161-
162-
describe('className / class massaging', () => {
163-
it('should render class using className', () => {
164-
let rendered = render(<div className="foo bar" />);
165-
expect(rendered).to.equal('<div class="foo bar"></div>');
166-
});
167-
168-
it('should render class using class', () => {
169-
let rendered = render(<div class="foo bar" />);
170-
expect(rendered).to.equal('<div class="foo bar"></div>');
171-
});
172-
173-
it('should prefer className over class', () => {
174-
let rendered = render(<div class="foo" className="foo bar" />);
175-
expect(rendered).to.equal('<div class="foo bar"></div>');
176-
});
177-
});
178-
});
179-
180-
describe('shallowRender()', () => {
181-
it('should not render nested components', () => {
182-
let Test = spy( ({ foo, children }) => <div bar={foo}><b>test child</b>{ children }</div> );
183-
Test.displayName = 'Test';
184-
185-
let rendered = shallowRender(
186-
<section>
187-
<Test foo={1}><span>asdf</span></Test>
188-
</section>
189-
);
190-
191-
expect(rendered).to.equal(`<section><Test foo="1"><span>asdf</span></Test></section>`);
192-
expect(Test).not.to.have.been.called;
193-
});
194-
195-
it('should always render root component', () => {
196-
let Test = spy( ({ foo, children }) => <div bar={foo}><b>test child</b>{ children }</div> );
197-
Test.displayName = 'Test';
198-
199-
let rendered = shallowRender(
200-
<Test foo={1}>
201-
<span>asdf</span>
202-
</Test>
203-
);
204-
205-
expect(rendered).to.equal(`<div bar="1"><b>test child</b><span>asdf</span></div>`);
206-
expect(Test).to.have.been.calledOnce;
15+
it('exposes shallowRender as a named export', () => {
16+
expect(shallowRender).to.be.a('function');
20717
});
20818
});
20919
});

0 commit comments

Comments
 (0)