Skip to content

Commit

Permalink
feat: allow custom container component and more customization
Browse files Browse the repository at this point in the history
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
  • Loading branch information
joshswan committed Mar 20, 2020
1 parent 561d265 commit b22f4e7
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 34 deletions.
82 changes: 66 additions & 16 deletions src/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

exports[`<Autolink /> does not match scheme-containing url when schemeMatches disabled 1`] = `
<Text>
http://github.com
<Text>
http://github.com
</Text>
</Text>
`;

exports[`<Autolink /> does not match top-level domain url when wwwMatches disabled 1`] = `
<Text>
github.com
<Text>
github.com
</Text>
</Text>
`;

exports[`<Autolink /> does not match www-containing url when wwwMatches disabled 1`] = `
<Text>
www.github.com
<Text>
www.github.com
</Text>
</Text>
`;

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

exports[`<Autolink /> does not wrap a hashtag in a link Text node when hashtag prop disabled 1`] = `
<Text>
#awesome
<Text>
#awesome
</Text>
</Text>
`;

exports[`<Autolink /> does not wrap a latitude/longitude pair in a link Text node when latlng prop disabled 1`] = `
<Text>
34.0522, -118.2437
<Text>
34.0522, -118.2437
</Text>
</Text>
`;

exports[`<Autolink /> does not wrap a mention/handle in a link Text node when mention prop disabled 1`] = `
<Text>
@twitter
<Text>
@twitter
</Text>
</Text>
`;

exports[`<Autolink /> does not wrap a phone number in a link Text node when phone prop disabled 1`] = `
<Text>
415-555-5555
<Text>
415-555-5555
</Text>
</Text>
`;

exports[`<Autolink /> does not wrap a url in a link Text node when url prop disabled 1`] = `
<Text>
https://github.com/joshswan/react-native-autolink
<Text>
https://github.com/joshswan/react-native-autolink
</Text>
</Text>
`;

exports[`<Autolink /> does not wrap an email address in a link Text node when email prop disabled 1`] = `
<Text>
josh@example.com
<Text>
josh@example.com
</Text>
</Text>
`;

exports[`<Autolink /> links multiple elements individually 1`] = `
<Text>
Hi
<Text>
Hi
</Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
Expand All @@ -116,7 +136,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
>
@josh
</Text>
(
<Text>
(
</Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
Expand All @@ -128,7 +150,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
>
josh@example.com
</Text>
or
<Text>
or
</Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
Expand All @@ -140,7 +164,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
>
415-555-5555
</Text>
), check out
<Text>
), check out
</Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
Expand All @@ -152,7 +178,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
>
github.com/joshswan/..e-autolink
</Text>
. It's
<Text>
. It's
</Text>
<Text
onLongPress={[Function]}
onPress={[Function]}
Expand All @@ -164,7 +192,9 @@ exports[`<Autolink /> links multiple elements individually 1`] = `
>
#awesome
</Text>
!
<Text>
!
</Text>
</Text>
`;

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

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

exports[`<Autolink /> renders a custom container node 1`] = `
<View>
<Text>
Testing
</Text>
</View>
`;

exports[`<Autolink /> renders a string when nothing to link 1`] = `
<Text>
Testing
<Text>
Testing
</Text>
</Text>
`;

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

exports[`<Autolink /> renders text using renderText prop if provided 1`] = `
<View>
<View>
<Text>
Testing
</Text>
</View>
</View>
`;

exports[`<Autolink /> replaces removed protion of truncated url with truncateChars prop value 1`] = `
<Text>
<Text
Expand Down
13 changes: 12 additions & 1 deletion src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import React from 'react';
import { Text } from 'react-native';
import { Text, View } from 'react-native';
import renderer from 'react-test-renderer';
import Autolink from '..';

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

test('renders a custom container node', () => {
const tree = renderer.create(<Autolink component={View} text="Testing" />).toJSON();
expect(tree).toMatchSnapshot();
});

test('wraps an email address with a link Text node when email prop enabled', () => {
const tree = renderer.create(<Autolink text="josh@example.com" email />).toJSON();
expect(tree).toMatchSnapshot();
Expand Down Expand Up @@ -185,6 +190,12 @@ describe('<Autolink />', () => {
expect(tree).toMatchSnapshot();
});

test('renders text using renderText prop if provided', () => {
const renderText = (text) => <View><Text>{text}</Text></View>;
const tree = renderer.create(<Autolink component={View} text="Testing" renderText={renderText} />).toJSON();
expect(tree).toMatchSnapshot();
});

test('calls onPress handler prop when link clicked', () => {
const onPress = jest.fn();
const tree = renderer.create(<Autolink text="josh@example.com" onPress={onPress} />);
Expand Down
48 changes: 31 additions & 17 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from 'react-native';
import * as Truncate from './truncate';
import { Matchers, MatcherId, LatLngMatch } from './matchers';
import { PropsOf } from './types';

const tagBuilder = new AnchorTagBuilder();

Expand All @@ -37,20 +38,24 @@ const styles = StyleSheet.create({
},
});

interface Props {
interface AutolinkProps<C extends React.ComponentType = React.ComponentType> {
component?: C;
email?: boolean;
hashtag?: false | 'facebook' | 'instagram' | 'twitter';
latlng?: boolean;
linkProps?: TextProps;
linkStyle?: StyleProp<TextStyle>;
mention?: false | 'instagram' | 'soundcloud' | 'twitter';
onPress?: (url: string, match: Match) => void;
onLongPress?: (url: string, match: Match) => void;
phone?: boolean | 'text' | 'sms';
renderLink?: (text: string, match: Match, index: number) => React.ReactNode;
renderText?: (text: string, index: number) => React.ReactNode;
showAlert?: boolean;
stripPrefix?: boolean;
stripTrailingSlash?: boolean;
text: string;
textProps?: TextProps;
truncate?: number;
truncateChars?: string;
truncateLocation?: 'end' | 'middle' | 'smart';
Expand All @@ -62,7 +67,13 @@ interface Props {
webFallback?: boolean;
}

export default class Autolink extends PureComponent<TextProps & Props> {
type Props<C extends React.ComponentType> = AutolinkProps<C> & Omit<
PropsOf<C>, keyof AutolinkProps
>;

export default class Autolink<
C extends React.ComponentType = typeof Text
> extends PureComponent<Props<C>> {
static truncate(text: string, {
truncate = 32,
truncateChars = '..',
Expand All @@ -88,11 +99,13 @@ export default class Autolink extends PureComponent<TextProps & Props> {
email: true,
hashtag: false,
latlng: false,
linkProps: {},
mention: false,
phone: true,
showAlert: false,
stripPrefix: true,
stripTrailingSlash: true,
textProps: {},
truncate: 32,
truncateChars: '..',
truncateLocation: 'smart',
Expand Down Expand Up @@ -216,19 +229,19 @@ export default class Autolink extends PureComponent<TextProps & Props> {
text: string,
match: Match,
index: number,
textProps: Partial<TextProps>,
textProps: Partial<TextProps> = {},
): ReactNode {
const { truncate, linkStyle } = this.props;
const truncated = truncate ? Autolink.truncate(text, this.props) : text;

return (
<Text
// eslint-disable-next-line react/jsx-props-no-spreading
{...textProps}
key={index}
style={linkStyle || styles.link}
onPress={() => this.onPress(match)}
onLongPress={() => this.onLongPress(match)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...textProps}
key={index}
>
{truncated}
</Text>
Expand All @@ -238,20 +251,23 @@ export default class Autolink extends PureComponent<TextProps & Props> {
render(): ReactNode {
const {
children,
component = Text,
email,
hashtag,
latlng,
linkProps,
linkStyle,
mention,
onPress,
onLongPress,
phone,
renderLink,
renderText,
showAlert,
stripPrefix,
stripTrailingSlash,
style,
text,
textProps,
truncate,
truncateChars,
truncateLocation,
Expand Down Expand Up @@ -322,26 +338,24 @@ export default class Autolink extends PureComponent<TextProps & Props> {
.map((part, index) => {
const match = matches[part];

if (!match) return part;

switch (match.getType()) {
switch (match?.getType()) {
case 'email':
case 'hashtag':
case 'latlng':
case 'mention':
case 'phone':
case 'url':
return (renderLink)
return renderLink
? renderLink(match.getAnchorText(), match, index)
: this.renderLink(match.getAnchorText(), match, index, other);
: this.renderLink(match.getAnchorText(), match, index, linkProps);
default:
return part;
return renderText
? renderText(part, index)
// eslint-disable-next-line react/jsx-props-no-spreading, react/no-array-index-key
: <Text {...textProps} key={index}>{part}</Text>;
}
});

return createElement(Text, {
style,
...other,
}, ...nodes);
return createElement(component, other, ...nodes);
}
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type PropsOf<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
E extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>
> = JSX.LibraryManagedAttributes<E, React.ComponentPropsWithRef<E>>;

0 comments on commit b22f4e7

Please sign in to comment.