Skip to content

Commit b3553fd

Browse files
authored
Refactor types w/ generics and add use functional components (#13)
1 parent 1119e34 commit b3553fd

File tree

13 files changed

+1744
-150
lines changed

13 files changed

+1744
-150
lines changed

example/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as React from 'react'
1+
import React from 'react'
22
import {hot} from 'react-hot-loader'
33

44
import {TextAnnotator, TokenAnnotator} from '../../src'

example/src/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as React from 'react'
2-
import * as ReactDOM from 'react-dom'
1+
import React from 'react'
2+
import ReactDOM from 'react-dom'
33

44
import App from './App'
55

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'jsdom',
4+
testPathIgnorePatterns: ['/node_modules/', '/.docz/', '/lib/'],
5+
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@
1212
"react-dom": "^16.8.0"
1313
},
1414
"devDependencies": {
15+
"@testing-library/react": "^10.0.2",
16+
"@types/jest": "^25.2.1",
1517
"@types/node": "^12.0.0",
1618
"@types/react": "^16.8.0",
1719
"@types/react-dom": "^16.8.0",
1820
"docz": "^2.2.0",
1921
"gh-pages": "^2.1.1",
22+
"jest": "^25.2.7",
2023
"prettier": "^1.19.1",
2124
"react": "^16.8.0",
2225
"react-dom": "^16.8.0",
2326
"react-hot-loader": "^4.0.1",
2427
"react-powerplug": "^1.0.0",
25-
"typescript": "^3.7.2"
28+
"ts-jest": "^25.3.1",
29+
"typescript": "^3.8.3"
2630
},
2731
"scripts": {
2832
"dev": "cd example && webpack-dev-server --hot --history-api-fallback --mode development",

src/Mark.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as React from 'react'
1+
import React from 'react'
22

33
export interface MarkProps {
44
key: string

src/TextAnnotator.tsx

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

33
import Mark from './Mark'
44
import {selectionIsEmpty, selectionIsBackwards, splitWithOffsets} from './utils'
5+
import {Span} from './span'
56

67
const Split = props => {
78
if (props.mark) return <Mark {...props} />
@@ -17,40 +18,29 @@ const Split = props => {
1718
)
1819
}
1920

20-
interface TextSpan {
21-
start: number
22-
end: number
21+
interface TextSpan extends Span {
2322
text: string
2423
}
2524

26-
export interface TextAnnotatorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
27-
style: object
25+
type TextBaseProps<T> = {
2826
content: string
29-
value: TextSpan[]
30-
onChange: (value: TextSpan[]) => any
31-
getSpan?: (span: TextSpan) => TextSpan
27+
value: T[]
28+
onChange: (value: T[]) => any
29+
getSpan?: (span: TextSpan) => T
3230
// TODO: determine whether to overwrite or leave intersecting ranges.
3331
}
3432

35-
class TextAnnotator extends React.Component<TextAnnotatorProps, {}> {
36-
rootRef: React.RefObject<HTMLDivElement>
33+
type TextAnnotatorProps<T> = React.HTMLAttributes<HTMLDivElement> & TextBaseProps<T>
3734

38-
constructor(props) {
39-
super(props)
40-
41-
this.rootRef = React.createRef()
42-
}
43-
44-
componentDidMount() {
45-
this.rootRef.current.addEventListener('mouseup', this.handleMouseUp)
46-
}
47-
48-
componentWillUnmount() {
49-
this.rootRef.current.removeEventListener('mouseup', this.handleMouseUp)
35+
const TextAnnotator = <T extends Span>(props: TextAnnotatorProps<T>) => {
36+
const getSpan = (span: TextSpan): T => {
37+
// TODO: Better typings here.
38+
if (props.getSpan) return props.getSpan(span) as T
39+
return {start: span.start, end: span.end} as T
5040
}
5141

52-
handleMouseUp = () => {
53-
if (!this.props.onChange) return
42+
const handleMouseUp = () => {
43+
if (!props.onChange) return
5444

5545
const selection = window.getSelection()
5646

@@ -67,41 +57,28 @@ class TextAnnotator extends React.Component<TextAnnotatorProps, {}> {
6757
;[start, end] = [end, start]
6858
}
6959

70-
this.props.onChange([
71-
...this.props.value,
72-
this.getSpan({start, end, text: this.props.content.slice(start, end)}),
73-
])
60+
props.onChange([...props.value, getSpan({start, end, text: content.slice(start, end)})])
7461

7562
window.getSelection().empty()
7663
}
7764

78-
handleSplitClick = ({start, end}) => {
65+
const handleSplitClick = ({start, end}) => {
7966
// Find and remove the matching split.
80-
const splitIndex = this.props.value.findIndex(s => s.start === start && s.end === end)
67+
const splitIndex = props.value.findIndex(s => s.start === start && s.end === end)
8168
if (splitIndex >= 0) {
82-
this.props.onChange([
83-
...this.props.value.slice(0, splitIndex),
84-
...this.props.value.slice(splitIndex + 1),
85-
])
69+
props.onChange([...props.value.slice(0, splitIndex), ...props.value.slice(splitIndex + 1)])
8670
}
8771
}
8872

89-
getSpan = (span: TextSpan) => {
90-
if (this.props.getSpan) return this.props.getSpan(span)
91-
return span
92-
}
93-
94-
render() {
95-
const {content, value, style} = this.props
96-
const splits = splitWithOffsets(content, value)
97-
return (
98-
<div style={style} ref={this.rootRef}>
99-
{splits.map(split => (
100-
<Split key={`${split.start}-${split.end}`} {...split} onClick={this.handleSplitClick} />
101-
))}
102-
</div>
103-
)
104-
}
73+
const {content, value, style} = props
74+
const splits = splitWithOffsets(content, value)
75+
return (
76+
<div style={style} onMouseUp={handleMouseUp}>
77+
{splits.map(split => (
78+
<Split key={`${split.start}-${split.end}`} {...split} onClick={handleSplitClick} />
79+
))}
80+
</div>
81+
)
10582
}
10683

10784
export default TextAnnotator

src/TokenAnnotator.tsx

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

33
import Mark, {MarkProps} from './Mark'
44
import {selectionIsEmpty, selectionIsBackwards, splitTokensWithOffsets} from './utils'
5+
import {Span} from './span'
56

67
interface TokenProps {
78
i: number
@@ -18,40 +19,26 @@ const Token: React.SFC<TokenProps> = props => {
1819
return <span data-i={props.i}>{props.content} </span>
1920
}
2021

21-
export interface TokenAnnotatorProps
22+
export interface TokenAnnotatorProps<T>
2223
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
2324
tokens: string[]
24-
value: TokenSpan[]
25-
onChange: (value: TokenSpan[]) => any
26-
getSpan?: (span: TokenSpan) => TokenSpan
25+
value: T[]
26+
onChange: (value: T[]) => any
27+
getSpan?: (span: TokenSpan) => T
2728
renderMark?: (props: MarkProps) => JSX.Element
2829
// TODO: determine whether to overwrite or leave intersecting ranges.
2930
}
3031

31-
// TODO: When React 16.3 types are ready, remove casts.
32-
class TokenAnnotator extends React.Component<TokenAnnotatorProps, {}> {
33-
static defaultProps = {
34-
renderMark: props => <Mark {...props} />,
35-
}
36-
37-
rootRef: React.RefObject<HTMLDivElement>
38-
39-
constructor(props) {
40-
super(props)
41-
42-
this.rootRef = React.createRef()
43-
}
32+
const TokenAnnotator = <T extends Span>(props: TokenAnnotatorProps<T>) => {
33+
const renderMark = props.renderMark || (props => <Mark {...props} />)
4434

45-
componentDidMount() {
46-
this.rootRef.current.addEventListener('mouseup', this.handleMouseUp)
35+
const getSpan = (span: TokenSpan): T => {
36+
if (props.getSpan) return props.getSpan(span)
37+
return {start: span.start, end: span.end} as T
4738
}
4839

49-
componentWillUnmount() {
50-
this.rootRef.current.removeEventListener('mouseup', this.handleMouseUp)
51-
}
52-
53-
handleMouseUp = () => {
54-
if (!this.props.onChange) return
40+
const handleMouseUp = () => {
41+
if (!props.onChange) return
5542

5643
const selection = window.getSelection()
5744

@@ -74,48 +61,35 @@ class TokenAnnotator extends React.Component<TokenAnnotatorProps, {}> {
7461

7562
end += 1
7663

77-
this.props.onChange([
78-
...this.props.value,
79-
this.getSpan({start, end, tokens: this.props.tokens.slice(start, end)}),
80-
])
64+
props.onChange([...props.value, getSpan({start, end, tokens: props.tokens.slice(start, end)})])
8165
window.getSelection().empty()
8266
}
8367

84-
handleSplitClick = ({start, end}) => {
68+
const handleSplitClick = ({start, end}) => {
8569
// Find and remove the matching split.
86-
const splitIndex = this.props.value.findIndex(s => s.start === start && s.end === end)
70+
const splitIndex = props.value.findIndex(s => s.start === start && s.end === end)
8771
if (splitIndex >= 0) {
88-
this.props.onChange([
89-
...this.props.value.slice(0, splitIndex),
90-
...this.props.value.slice(splitIndex + 1),
91-
])
72+
props.onChange([...props.value.slice(0, splitIndex), ...props.value.slice(splitIndex + 1)])
9273
}
9374
}
9475

95-
getSpan = (span: TokenSpan) => {
96-
if (this.props.getSpan) return this.props.getSpan(span)
97-
return span
98-
}
99-
100-
render() {
101-
const {tokens, value, renderMark, onChange, getSpan, ...divProps} = this.props
102-
const splits = splitTokensWithOffsets(tokens, value)
103-
return (
104-
<div ref={this.rootRef} {...divProps}>
105-
{splits.map((split, i) =>
106-
split.mark ? (
107-
renderMark({
108-
key: `${split.start}-${split.end}`,
109-
...split,
110-
onClick: this.handleSplitClick,
111-
})
112-
) : (
113-
<Token key={split.i} {...split} />
114-
)
115-
)}
116-
</div>
117-
)
118-
}
76+
const {tokens, value, onChange, getSpan: _, ...divProps} = props
77+
const splits = splitTokensWithOffsets(tokens, value)
78+
return (
79+
<div {...divProps} onMouseUp={handleMouseUp}>
80+
{splits.map((split, i) =>
81+
split.mark ? (
82+
renderMark({
83+
key: `${split.start}-${split.end}`,
84+
...split,
85+
onClick: handleSplitClick,
86+
})
87+
) : (
88+
<Token key={split.i} {...split} />
89+
)
90+
)}
91+
</div>
92+
)
11993
}
12094

12195
export default TokenAnnotator

src/__tests__/TextAnnotator.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react'
2+
import TextAnnotator from '../TextAnnotator'
3+
import {render, fireEvent} from '@testing-library/react'
4+
5+
test('renders without getSpan', () => {
6+
render(
7+
<TextAnnotator
8+
content="Foo bar baz"
9+
value={[{start: 0, end: 5, tag: 'PERSON', text: 'foo', extra: 1}]}
10+
onChange={() => {}}
11+
/>
12+
)
13+
})
14+
15+
test('renders when value and getSpan return match', () => {
16+
render(
17+
<TextAnnotator
18+
content="Foo bar baz"
19+
value={[{start: 0, end: 5, tag: 'PERSON', text: 'foo', extra: 1}]}
20+
onChange={() => {}}
21+
getSpan={span => ({...span, tag: 'FOO', text: 'foo', extra: 1})}
22+
/>
23+
)
24+
})

src/__tests__/TokenAnnotator.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react'
2+
import {render} from '@testing-library/react'
3+
import TokenAnnotator from '../TokenAnnotator'
4+
5+
test('renders without getSpan', () => {
6+
render(
7+
<TokenAnnotator
8+
tokens={['Foo', 'Bar', 'Baz']}
9+
value={[{start: 0, end: 5, tag: 'PERSON', tokens: [], extra: 1}]}
10+
onChange={() => {}}
11+
/>
12+
)
13+
})
14+
15+
test('renders when value and getSpan return match', () => {
16+
render(
17+
<TokenAnnotator
18+
tokens={['Foo', 'Bar', 'Baz']}
19+
value={[{start: 0, end: 1, tag: 'PERSON', tokens: ['Foo'], extra: 1}]}
20+
onChange={() => {}}
21+
getSpan={span => ({...span, tag: 'FOO', tokens: ['Foo'], extra: 1})}
22+
/>
23+
)
24+
})

src/span.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Span = {
2+
start: number
3+
end: number
4+
}

0 commit comments

Comments
 (0)