react-native-controlled-mentions

Fully controlled React Native mentions component

Usage no npm install needed!

<script type="module">
  import reactNativeControlledMentions from 'https://cdn.skypack.dev/react-native-controlled-mentions';
</script>

README

react-native-controlled-mentions npm version

Pretty simple and fully controlled mention input. It can:

  • Gracefully render formatted mentions directly in RN TextInput component
  • Support for different mention types (@user mentions, #hashtags, etc)
  • Use value/onChange as in usual TextInput props
  • Completely typed (written on TypeScript)
  • No need for native libraries

In addition, you can add custom styling for a regex pattern (like URLs) using the optimized recursive function for parsing the value.

Demo

Try it on Expo Snack: https://snack.expo.io/@dabakovich/mentionsapp

Getting started

Install the library using either Yarn:

yarn add react-native-controlled-mentions

or npm:

npm install --save react-native-controlled-mentions

Usage

Import the MentionInput component:

import { MentionInput } from 'react-native-controlled-mentions'

Replace your TextInput by MentionInput component and add the partTypes property where you can define what mention or pattern types you want to support. It takes an array of PartType objects.

<MentionInput
  value={value}
  onChange={setValue}

  partTypes={[
    {
      trigger: '@', // Should be a single character like '@' or '#'
      renderSuggestions,
      textStyle: {fontWeight: 'bold', color: 'blue'}, // The mention style in the input
    },
  ]}
/>

Define your renderSuggestions functional component that receive MentionSuggestionsProps:

const suggestions = [
  {id: '1', name: 'David Tabaka'},
  {id: '2', name: 'Mary'},
  {id: '3', name: 'Tony'},
  {id: '4', name: 'Mike'},
  {id: '5', name: 'Grey'},
];

const renderSuggestions: FC<MentionSuggestionsProps> = ({keyword, onSuggestionPress}) => {
  if (keyword == null) {
    return null;
  }

  return (
    <View>
      {suggestions
        .filter(one => one.name.toLocaleLowerCase().includes(keyword.toLocaleLowerCase()))
        .map(one => (
          <Pressable
            key={one.id}
            onPress={() => onSuggestionPress(one)}

            style={{padding: 12}}
          >
            <Text>{one.name}</Text>
          </Pressable>
        ))
      }
    </View>
  );
};

You're done!

The whole example is in the /example folder.

API

MentionInput component props

Property name Description Type Required Default
value The same as in TextInput string true
onChange The same as in TextInput (value: string) => void true
partTypes Declare what part types you want to support (mentions, hashtags, urls) PartType[] false []
inputRef Reference to the TextInput component inside MentionInput Ref<TextInput> false
containerStyle Style to the MentionInput's root component StyleProp<TextStyle> false
...textInputProps Other text input props TextInputProps false

PartType type

MentionPartType | PatternPartType

MentionPartType type props

Property name Description Type Required Default
trigger Character that will trigger current mention type string true
renderSuggestions Renderer for mention suggestions component (props: MentionSuggestionsProps) => ReactNode false
allowedSpacesCount How much spaces are allowed for mention keyword number false 1
isInsertSpaceAfterMention Should we add a space after selected mentions if the mention is at the end of row boolean false false
isBottomMentionSuggestionsRender Should we render either at the top or bottom of the input boolean false
textStyle Text style for mentions in TextInput StyleProp<TextStyle> false
getPlainString Function for generating custom mention text in text input (mention: MentionData) => string false

PatternPartType type props

Property name Description Type Required Default
pattern RegExp for parsing a pattern, should include global flag RegExp true
textStyle Text style for pattern in TextInput StyleProp<TextStyle> false

MentionSuggestionsProps type props

keyword: string | undefined

Keyword that will provide string between trigger character (e.g. '@') and cursor.

If the cursor is not tracking any mention typing the keyword will be undefined.

Examples where @name is just plain text yet, not mention and | is cursor position:

'|abc @name dfg' - keyword is undefined
'abc @| dfg' - keyword is ''
'abc @name| dfg' - keyword is 'name'
'abc @na|me dfg' - keyword is 'na'
'abc @|name dfg' - keyword is against ''
'abc @name |dfg' - keyword is against undefined

onSuggestionPress: (suggestion: Suggestion) => void

You should call that callback when user selects any suggestion.

Suggestion type props

id: string

Unique id for each suggestion.

name: string

Name that will be shown in MentionInput when user will select the suggestion.

MentionData type props

For example, we have that mention value @[David Tabaka](123). Then after parsing that string by mentionRegEx we will get next properties:

original: string

The whole mention value string - @[David Tabaka](123)

trigger: string

The extracted trigger - @

name: string

The extracted name - David Tabaka

id: string

The extracted id - 123

mentionRegEx

/(?<original>(?<trigger>.)\[(?<name>([^[]*))]\((?<id>([\d\w-]*))\))/gi;

Parsing MentionInput's value

You can import RegEx that is using in the component and then extract all your mentions from MentionInput's value using your own logic.

import { mentionRegEx } from 'react-native-controlled-mentions';

Or you can use replaceMentionValues helper to replace all mentions from MentionInput's input using your replacer function that receives MentionData type and returns string.

import { replaceMentionValues } from 'react-native-controlled-mentions';

const value = 'Hello @[David Tabaka](5)! How are you?';

console.log(replaceMentionValues(value, ({id}) => `@${id}`)); // Hello @5! How are you?
console.log(replaceMentionValues(value, ({name}) => `@${name}`)); // Hello @David Tabaka! How are you?

Rendering MentionInput's value

If you want to parse and render your value somewhere else you can use parseValue tool which gives you array of parts and then use your own part renderer to resolve this issue.

Here is an example:

import {
  Part,
  PartType,
  parseValue,
  isMentionPartType,
} from 'react-native-controlled-mentions';

/**
 * Part renderer
 * 
 * @param part
 * @param index
 */
const renderPart = (
  part: Part,
  index: number,
) => {
  // Just plain text
  if (!part.partType) {
    return <Text key={index}>{part.text}</Text>;
  }

  // Mention type part
  if (isMentionPartType(part.partType)) {
    return (
      <Text
        key={`${index}-${part.data?.trigger}`}
        style={part.partType.textStyle}
        onPress={() => console.log('Pressed', part.data)}
      >
        {part.text}
      </Text>
    );
  }

  // Other styled part types
  return (
    <Text
      key={`${index}-pattern`}
      style={part.partType.textStyle}
    >
      {part.text}
    </Text>
  );
};

/**
 * Value renderer. Parsing value to parts array and then mapping the array using 'renderPart'
 * 
 * @param value - value from MentionInput
 * @param partTypes - the part types array that you providing to MentionInput
 */
const renderValue: FC = (
  value: string,
  partTypes: PartType[],
) => {
  const {parts} = parseValue(value, partTypes);

  return <Text>{parts.map(renderPart)}</Text>;
};

To Do

  • Add support for different text formatting (e.g. URLs)
  • Add more customizations DONE
  • Add ability to handle few mention types ("#", "@" etc) DONE

Known issues

  • Mention name regex accepts white spaces (e.g. {name: ' ', value: 1})
  • Keyboard auto-correction not working if suggested word has the same length FIXED
  • Text becomes transparent when setting custom font size in TextInput FIXED

Support Me

Buy Me A Coffee