Skip to content

Commit 9205dc4

Browse files
Breaking: Replace eventHandlers by overrides
1 parent 25ff095 commit 9205dc4

File tree

7 files changed

+94
-80
lines changed

7 files changed

+94
-80
lines changed

README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# htmlstring-to-react
22

3+
## Why ?
4+
5+
This module provide an easy way to parse a string containing html elements to an array of React elements. It tries to focus to security (using [DOMPurify](https://github.com/cure53/DOMPurify)) and keeping the bundle as small as possible
6+
7+
It is heavily inspired by [html2react](https://github.com/Deschtex/html2react) and [html-react-parser](https://github.com/remarkablemark/html-react-parser)
8+
39
## How to install ?
410

511
npm install htmlstring-to-react
@@ -13,30 +19,37 @@
1319
import { parse } from 'htmlstring-to-react'
1420
parse('<em key="1"><b key="2">It\' is working</b></em>')
1521

16-
### Add an event handler
22+
### Add an override
1723

18-
Add an evant handler on all `b` elements
24+
You can use css selectors to override an element
1925

2026
import { parse } from 'htmlstring-to-react'
2127
parse('<b key="1">It</b> is <b key="2">working</b>', {
22-
eventHandlers: {
23-
b: {
24-
onClick: () => console.log('click'),
25-
},
28+
overrides: {
29+
b: (props, textContent) => <b onClick={console.log('Click')}>{textContent}</b>
2630
},
2731
})
2832

29-
Each key in `eventHandlers` can be a valid css selector.
33+
All valid css selectors works
3034

3135
import { parse } from 'htmlstring-to-react'
3236
parse('<b key="1">It</b> is <b key="2" class="active">working</b>', {
33-
eventHandlers: {
34-
'b.active': {
35-
onClick: () => console.log('click'),
36-
},
37+
overrides: {
38+
'b.active': (props, textContent) => <b onClick={console.log('Click')}>{textContent}</b>
3739
},
3840
})
3941

42+
**IMPORTANT** Overrides do not support nested elements in the current stage, so this code
43+
44+
import { parse } from 'htmlstring-to-react'
45+
parse('<b key="1"><b key="2">It is working</b></b>', {
46+
overrides: {
47+
b: (props, textContent) => <b onClick={console.log('Click')}>{textContent}</b>
48+
},
49+
})
50+
51+
will drop the inner `b` but keep the textContent
52+
4053
### Change dom parsing configuration
4154

4255
By default, we are sanitizing the html input using `DOMPurify` module. You can override the configuration we are using

src/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SelectorsToEventHandlers } from './event'
1+
import { SelectorsToElement } from './override'
22

33
function isObject(item: any) {
44
return (item && typeof item === 'object' && !Array.isArray(item))
@@ -63,7 +63,7 @@ export interface DOMConfig {
6363
*/
6464
export interface Config {
6565
dom?: DOMConfig
66-
eventHandlers?: SelectorsToEventHandlers
66+
overrides?: SelectorsToElement
6767
}
6868

6969
/**

src/event.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Config, getConfig } from './config'
22
import * as dom from './dom'
3-
import { addEventHandlers } from './event'
3+
import { override } from './override'
44
import { render } from './react'
55

66
export function parse(html: string, userOptions?: Config): React.ReactNode[] {
@@ -11,8 +11,8 @@ export function parse(html: string, userOptions?: Config): React.ReactNode[] {
1111
const options = getConfig(userOptions)
1212
const document = dom.parse(html, options)
1313

14-
if (options.eventHandlers) {
15-
addEventHandlers(document, options.eventHandlers)
14+
if (options.overrides) {
15+
override(document, options.overrides)
1616
}
1717

1818
return render(document.childNodes)

src/override.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* An internal representation of an element that can include a React element to use as an override
3+
*/
4+
export interface EnrichedElement extends Element {
5+
override?: (props: any, textContent?: string) => React.ReactElement<any>
6+
}
7+
8+
/**
9+
* A map between a css selector and a React element
10+
*/
11+
export interface SelectorsToElement {
12+
[keyof: string]: (props?: any, textContent?: string) => React.ReactElement<any>
13+
}
14+
15+
export function override(document: DocumentFragment, selectorsToElement: SelectorsToElement) {
16+
Object.keys(selectorsToElement).forEach((selector) => {
17+
const reactElement = selectorsToElement[selector]
18+
19+
const elements = document.querySelectorAll(selector)
20+
for (let i = 0; i < elements.length; i ++) {
21+
const element = elements.item(i) as EnrichedElement
22+
element.override = reactElement
23+
}
24+
})
25+
}

src/react.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react'
22

33
import { getAttributes, NodeType } from './dom'
4-
import { EnrichedElement, EventHandlers } from './event'
4+
import { EnrichedElement } from './override'
55

66
export interface Attributes { [keyof: string]: string | React.EventHandler<any> }
77

@@ -53,16 +53,9 @@ const reactAttributesMap: { [keyof: string]: string } = {
5353
usemap: 'useMap',
5454
}
5555

56-
function transformAttributes(attributesMap: NamedNodeMap, eventHandlers: EventHandlers): Attributes {
56+
function transformAttributes(attributesMap: NamedNodeMap): Attributes {
5757
const attributes = getAttributes(attributesMap)
5858
const transformedAttributes: Attributes = {}
59-
60-
if (eventHandlers) {
61-
Object.keys(eventHandlers).forEach((eventHandler) => {
62-
transformedAttributes[eventHandler] = eventHandlers[eventHandler]
63-
})
64-
}
65-
6659
Object.keys(attributes).forEach((key) => {
6760
if (reactAttributesMap[key]) {
6861
transformedAttributes[reactAttributesMap[key]] = attributes[key]
@@ -79,6 +72,11 @@ function renderTextNode(node: Node & ChildNode) {
7972

8073
function renderElementNode(node: Node & ChildNode) {
8174
const element = transform(node as EnrichedElement)
75+
76+
if (element.override) {
77+
return React.cloneElement(element.override(element.attributes, node.textContent))
78+
}
79+
8280
if (element.childNodes) {
8381
return React.createElement(element.nodeName, element.attributes, render(element.childNodes))
8482
}
@@ -87,11 +85,12 @@ function renderElementNode(node: Node & ChildNode) {
8785

8886
function transform(element: EnrichedElement) {
8987
return {
90-
attributes: transformAttributes(element.attributes, element.eventHandlers),
88+
attributes: transformAttributes(element.attributes),
9189
childNodes: element.childNodes,
9290
nodeName: element.nodeName.toLowerCase(),
9391
nodeType: element.nodeType,
9492
nodeValue: element.nodeValue,
93+
override: element.override,
9594
}
9695
}
9796

tests/api.spec.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,47 +25,59 @@ describe('Public API', () => {
2525
expect(wrapper.find('b')).toHaveLength(1)
2626
})
2727

28-
it('should accept event handlers', () => {
28+
it('should override using a React element', () => {
2929
const spy = jest.fn()
3030
const elements = htmlStringToReact.parse('<em key="1"><b key="2">It\' is working</b></em>', {
31-
eventHandlers: {
32-
b: {
33-
onClick: spy,
34-
},
31+
overrides: {
32+
b: (props, textContent) => React.createElement('b', { onClick: spy, key: props.key }, textContent),
3533
},
3634
})
3735
const wrapper = shallow(React.createElement('div', null, elements))
3836
expect(wrapper.text()).toEqual('It\' is working')
3937
expect(wrapper.find('em')).toHaveLength(1)
4038
expect(wrapper.find('b')).toHaveLength(1)
39+
expect(wrapper.find('b').key()).toEqual('2')
4140
expect(wrapper.find('b').simulate('click'))
4241
expect(spy).toHaveBeenCalled()
4342
})
4443

45-
it('should ignore incorrect event handlers', () => {
44+
it('should override all elements that match the selector', () => {
4645
const spy = jest.fn()
47-
const elements = htmlStringToReact.parse('<em key="1"><b key="2">It\' is working</b></em>', {
48-
eventHandlers: {
49-
b: {
50-
onclick: spy,
51-
},
46+
const elements = htmlStringToReact.parse('<b key="1">It</b> is <b key="2">working</b>', {
47+
overrides: {
48+
b: (props, textContent) => React.createElement('b', { onClick: spy }, textContent),
5249
},
5350
})
5451
const wrapper = shallow(React.createElement('div', null, elements))
55-
expect(wrapper.text()).toEqual('It\' is working')
56-
expect(wrapper.find('em')).toHaveLength(1)
52+
expect(wrapper.text()).toEqual('It is working')
53+
expect(wrapper.find('b')).toHaveLength(2)
54+
expect(wrapper.find('b').at(0).key()).toBeUndefined()
55+
expect(wrapper.find('b').at(1).key()).toBeUndefined()
56+
expect(wrapper.find('b').at(0).simulate('click'))
57+
expect(wrapper.find('b').at(1).simulate('click'))
58+
expect(spy).toHaveBeenCalledTimes(2)
59+
})
60+
61+
it('should get textContent even on nested elements', () => {
62+
const spy = jest.fn()
63+
const elements = htmlStringToReact.parse('<b key="1"><b key="2">It is working</b></b>', {
64+
overrides: {
65+
b: (props, textContent) => React.createElement('b', { onClick: spy }, textContent),
66+
},
67+
})
68+
const wrapper = shallow(React.createElement('div', null, elements))
69+
expect(wrapper.text()).toEqual('It is working')
5770
expect(wrapper.find('b')).toHaveLength(1)
58-
expect(wrapper.find('b').simulate('click'))
59-
expect(spy).toHaveBeenCalledTimes(0)
71+
expect(wrapper.find('b').at(0).key()).toBeUndefined()
72+
expect(wrapper.find('b').at(0).simulate('click'))
73+
expect(spy).toHaveBeenCalledTimes(1)
6074
})
6175

6276
it('should ignore incorrect selectors', () => {
6377
const spy = jest.fn()
6478
const elements = htmlStringToReact.parse('<em key="1"><b key="2">It\' is working</b></em>', {
65-
eventHandlers: {
66-
span: {
67-
onClick: spy,
68-
},
79+
overrides: {
80+
span: () => null,
6981
},
7082
})
7183
const wrapper = shallow(React.createElement('div', null, elements))

0 commit comments

Comments
 (0)