Skip to content

Commit b22f4e7

Browse files
committed
feat: allow custom container component and more customization
By default, Autolink will continue to behave as before: output will be wrapped in a single Text node and non-Autolink props will be passed through to that node. To allow for more customization, you can now specify a custom component, for example View, to use as the container instead of Text using the `component` prop. Any non-Autolink props will be passed through to this component instead, and all text within is wrapped with Text components as required by RN. Additionally, the new `linkProps` and `textProps` props allow you to pass any props to links or text components, respectively. And the new `renderText` prop allows you to completely customize how text is wrapped in the output. BREAKING CHANGE: Non-Autolink props are no longer passed to links. Only styles supplied to `linkStyle` and props supplied to `linkProps` are used when rendering links. You are still free to use `renderLink` to fully customize link rendering. closes #48
1 parent 561d265 commit b22f4e7

File tree

4 files changed

+113
-34
lines changed

4 files changed

+113
-34
lines changed

src/__tests__/__snapshots__/index.test.tsx.snap

+66-16
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@
22

33
exports[`<Autolink /> does not match scheme-containing url when schemeMatches disabled 1`] = `
44
<Text>
5-
http://github.com
5+
<Text>
6+
http://github.com
7+
</Text>
68
</Text>
79
`;
810

911
exports[`<Autolink /> does not match top-level domain url when wwwMatches disabled 1`] = `
1012
<Text>
11-
github.com
13+
<Text>
14+
github.com
15+
</Text>
1216
</Text>
1317
`;
1418

1519
exports[`<Autolink /> does not match www-containing url when wwwMatches disabled 1`] = `
1620
<Text>
17-
www.github.com
21+
<Text>
22+
www.github.com
23+
</Text>
1824
</Text>
1925
`;
2026

@@ -68,43 +74,57 @@ exports[`<Autolink /> does not truncate urls when zero is passed for truncate pr
6874

6975
exports[`<Autolink /> does not wrap a hashtag in a link Text node when hashtag prop disabled 1`] = `
7076
<Text>
71-
#awesome
77+
<Text>
78+
#awesome
79+
</Text>
7280
</Text>
7381
`;
7482

7583
exports[`<Autolink /> does not wrap a latitude/longitude pair in a link Text node when latlng prop disabled 1`] = `
7684
<Text>
77-
34.0522, -118.2437
85+
<Text>
86+
34.0522, -118.2437
87+
</Text>
7888
</Text>
7989
`;
8090

8191
exports[`<Autolink /> does not wrap a mention/handle in a link Text node when mention prop disabled 1`] = `
8292
<Text>
83-
@twitter
93+
<Text>
94+
@twitter
95+
</Text>
8496
</Text>
8597
`;
8698

8799
exports[`<Autolink /> does not wrap a phone number in a link Text node when phone prop disabled 1`] = `
88100
<Text>
89-
415-555-5555
101+
<Text>
102+
415-555-5555
103+
</Text>
90104
</Text>
91105
`;
92106

93107
exports[`<Autolink /> does not wrap a url in a link Text node when url prop disabled 1`] = `
94108
<Text>
95-
https://github.com/joshswan/react-native-autolink
109+
<Text>
110+
https://github.com/joshswan/react-native-autolink
111+
</Text>
96112
</Text>
97113
`;
98114

99115
exports[`<Autolink /> does not wrap an email address in a link Text node when email prop disabled 1`] = `
100116
<Text>
101-
117+
<Text>
118+
119+
</Text>
102120
</Text>
103121
`;
104122

105123
exports[`<Autolink /> links multiple elements individually 1`] = `
106124
<Text>
107-
Hi
125+
<Text>
126+
Hi
127+
</Text>
108128
<Text
109129
onLongPress={[Function]}
110130
onPress={[Function]}
@@ -116,7 +136,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
116136
>
117137
@josh
118138
</Text>
119-
(
139+
<Text>
140+
(
141+
</Text>
120142
<Text
121143
onLongPress={[Function]}
122144
onPress={[Function]}
@@ -128,7 +150,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
128150
>
129151
130152
</Text>
131-
or
153+
<Text>
154+
or
155+
</Text>
132156
<Text
133157
onLongPress={[Function]}
134158
onPress={[Function]}
@@ -140,7 +164,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
140164
>
141165
415-555-5555
142166
</Text>
143-
), check out
167+
<Text>
168+
), check out
169+
</Text>
144170
<Text
145171
onLongPress={[Function]}
146172
onPress={[Function]}
@@ -152,7 +178,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
152178
>
153179
github.com/joshswan/..e-autolink
154180
</Text>
155-
. It's
181+
<Text>
182+
. It's
183+
</Text>
156184
<Text
157185
onLongPress={[Function]}
158186
onPress={[Function]}
@@ -164,7 +192,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
164192
>
165193
#awesome
166194
</Text>
167-
!
195+
<Text>
196+
!
197+
</Text>
168198
</Text>
169199
`;
170200

@@ -218,9 +248,19 @@ exports[`<Autolink /> removes url trailing slashes when stripTrailingSlash prop
218248

219249
exports[`<Autolink /> renders a Text node 1`] = `<Text />`;
220250

251+
exports[`<Autolink /> renders a custom container node 1`] = `
252+
<View>
253+
<Text>
254+
Testing
255+
</Text>
256+
</View>
257+
`;
258+
221259
exports[`<Autolink /> renders a string when nothing to link 1`] = `
222260
<Text>
223-
Testing
261+
<Text>
262+
Testing
263+
</Text>
224264
</Text>
225265
`;
226266

@@ -232,6 +272,16 @@ exports[`<Autolink /> renders links using renderLink prop if provided 1`] = `
232272
</Text>
233273
`;
234274

275+
exports[`<Autolink /> renders text using renderText prop if provided 1`] = `
276+
<View>
277+
<View>
278+
<Text>
279+
Testing
280+
</Text>
281+
</View>
282+
</View>
283+
`;
284+
235285
exports[`<Autolink /> replaces removed protion of truncated url with truncateChars prop value 1`] = `
236286
<Text>
237287
<Text

src/__tests__/index.test.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import React from 'react';
10-
import { Text } from 'react-native';
10+
import { Text, View } from 'react-native';
1111
import renderer from 'react-test-renderer';
1212
import Autolink from '..';
1313

@@ -22,6 +22,11 @@ describe('<Autolink />', () => {
2222
expect(tree).toMatchSnapshot();
2323
});
2424

25+
test('renders a custom container node', () => {
26+
const tree = renderer.create(<Autolink component={View} text="Testing" />).toJSON();
27+
expect(tree).toMatchSnapshot();
28+
});
29+
2530
test('wraps an email address with a link Text node when email prop enabled', () => {
2631
const tree = renderer.create(<Autolink text="[email protected]" email />).toJSON();
2732
expect(tree).toMatchSnapshot();
@@ -185,6 +190,12 @@ describe('<Autolink />', () => {
185190
expect(tree).toMatchSnapshot();
186191
});
187192

193+
test('renders text using renderText prop if provided', () => {
194+
const renderText = (text) => <View><Text>{text}</Text></View>;
195+
const tree = renderer.create(<Autolink component={View} text="Testing" renderText={renderText} />).toJSON();
196+
expect(tree).toMatchSnapshot();
197+
});
198+
188199
test('calls onPress handler prop when link clicked', () => {
189200
const onPress = jest.fn();
190201
const tree = renderer.create(<Autolink text="[email protected]" onPress={onPress} />);

src/index.tsx

+31-17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from 'react-native';
2929
import * as Truncate from './truncate';
3030
import { Matchers, MatcherId, LatLngMatch } from './matchers';
31+
import { PropsOf } from './types';
3132

3233
const tagBuilder = new AnchorTagBuilder();
3334

@@ -37,20 +38,24 @@ const styles = StyleSheet.create({
3738
},
3839
});
3940

40-
interface Props {
41+
interface AutolinkProps<C extends React.ComponentType = React.ComponentType> {
42+
component?: C;
4143
email?: boolean;
4244
hashtag?: false | 'facebook' | 'instagram' | 'twitter';
4345
latlng?: boolean;
46+
linkProps?: TextProps;
4447
linkStyle?: StyleProp<TextStyle>;
4548
mention?: false | 'instagram' | 'soundcloud' | 'twitter';
4649
onPress?: (url: string, match: Match) => void;
4750
onLongPress?: (url: string, match: Match) => void;
4851
phone?: boolean | 'text' | 'sms';
4952
renderLink?: (text: string, match: Match, index: number) => React.ReactNode;
53+
renderText?: (text: string, index: number) => React.ReactNode;
5054
showAlert?: boolean;
5155
stripPrefix?: boolean;
5256
stripTrailingSlash?: boolean;
5357
text: string;
58+
textProps?: TextProps;
5459
truncate?: number;
5560
truncateChars?: string;
5661
truncateLocation?: 'end' | 'middle' | 'smart';
@@ -62,7 +67,13 @@ interface Props {
6267
webFallback?: boolean;
6368
}
6469

65-
export default class Autolink extends PureComponent<TextProps & Props> {
70+
type Props<C extends React.ComponentType> = AutolinkProps<C> & Omit<
71+
PropsOf<C>, keyof AutolinkProps
72+
>;
73+
74+
export default class Autolink<
75+
C extends React.ComponentType = typeof Text
76+
> extends PureComponent<Props<C>> {
6677
static truncate(text: string, {
6778
truncate = 32,
6879
truncateChars = '..',
@@ -88,11 +99,13 @@ export default class Autolink extends PureComponent<TextProps & Props> {
8899
email: true,
89100
hashtag: false,
90101
latlng: false,
102+
linkProps: {},
91103
mention: false,
92104
phone: true,
93105
showAlert: false,
94106
stripPrefix: true,
95107
stripTrailingSlash: true,
108+
textProps: {},
96109
truncate: 32,
97110
truncateChars: '..',
98111
truncateLocation: 'smart',
@@ -216,19 +229,19 @@ export default class Autolink extends PureComponent<TextProps & Props> {
216229
text: string,
217230
match: Match,
218231
index: number,
219-
textProps: Partial<TextProps>,
232+
textProps: Partial<TextProps> = {},
220233
): ReactNode {
221234
const { truncate, linkStyle } = this.props;
222235
const truncated = truncate ? Autolink.truncate(text, this.props) : text;
223236

224237
return (
225238
<Text
226-
// eslint-disable-next-line react/jsx-props-no-spreading
227-
{...textProps}
228-
key={index}
229239
style={linkStyle || styles.link}
230240
onPress={() => this.onPress(match)}
231241
onLongPress={() => this.onLongPress(match)}
242+
// eslint-disable-next-line react/jsx-props-no-spreading
243+
{...textProps}
244+
key={index}
232245
>
233246
{truncated}
234247
</Text>
@@ -238,20 +251,23 @@ export default class Autolink extends PureComponent<TextProps & Props> {
238251
render(): ReactNode {
239252
const {
240253
children,
254+
component = Text,
241255
email,
242256
hashtag,
243257
latlng,
258+
linkProps,
244259
linkStyle,
245260
mention,
246261
onPress,
247262
onLongPress,
248263
phone,
249264
renderLink,
265+
renderText,
250266
showAlert,
251267
stripPrefix,
252268
stripTrailingSlash,
253-
style,
254269
text,
270+
textProps,
255271
truncate,
256272
truncateChars,
257273
truncateLocation,
@@ -322,26 +338,24 @@ export default class Autolink extends PureComponent<TextProps & Props> {
322338
.map((part, index) => {
323339
const match = matches[part];
324340

325-
if (!match) return part;
326-
327-
switch (match.getType()) {
341+
switch (match?.getType()) {
328342
case 'email':
329343
case 'hashtag':
330344
case 'latlng':
331345
case 'mention':
332346
case 'phone':
333347
case 'url':
334-
return (renderLink)
348+
return renderLink
335349
? renderLink(match.getAnchorText(), match, index)
336-
: this.renderLink(match.getAnchorText(), match, index, other);
350+
: this.renderLink(match.getAnchorText(), match, index, linkProps);
337351
default:
338-
return part;
352+
return renderText
353+
? renderText(part, index)
354+
// eslint-disable-next-line react/jsx-props-no-spreading, react/no-array-index-key
355+
: <Text {...textProps} key={index}>{part}</Text>;
339356
}
340357
});
341358

342-
return createElement(Text, {
343-
style,
344-
...other,
345-
}, ...nodes);
359+
return createElement(component, other, ...nodes);
346360
}
347361
}

src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type PropsOf<
2+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3+
E extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
4+
> = JSX.LibraryManagedAttributes<E, React.ComponentPropsWithRef<E>>;

0 commit comments

Comments
 (0)