The Utility Component for an Annotated Text
Ruslan
Posted on November 18, 2022
This article is exploring the rc-marked-input react library that appears you to combine any components with editable text via annotations.
What is the problem?
An annotation is extra information associated with a piece of text. It can be used to add information about the desired visual presentation, as markup languages like HTML do:
As an annotation, it is not always handy to use the HTML markup for display:
Then control over a display component allow to simplify markup and use exotic visual representation and inner logic:
Moreover, it allows to use a markup to forward information for conditional display:
In summary, would like to have the following:
- Editable text;
- Custom markup;
- Mark - a component for display and/or change annotations;
- Overlay - a trigger component for annotations manipulating.
Combine these features appear to realize different components like highlight, autocomplete, mentions, tagged input, rich editor, etc. The Marked Input is the key component provided by the rc-marked-input library makes it possible to achieve this.
Marked Input
The following shows ways to interact with annotations, such as display, edit, add, and configure.
Mark to Display Annotations
The Marked Input is similar to the common input that allow to edit a text but required for the Mark
prop. It is a component that used for render annotations.
By default, the component uses the @[__label__](__value__)
markup to detect annotations. Then __label__
and __value__
from an annotation passed in the Mark
component like the label and the value prop.
Review the Clickable Highlight component:
import {MarkedInput} from "rc-marked-input";
const Mark = (props) => <mark onClick={_ => alert(props.value)}>{props.label}</mark>
const Marked = () => {
const [value, setValue] = useState("Hello, clickable marked @[world](Hello! Hello!)!")
return <MarkedInput Mark={Mark} value={value} onChange={setValue}/>
}
It uses the mark tag to display the label and the alert to display the value by the click of an annotation.
Using annotations to store the necessary information, a Mark component can use it to implement any logic with any exotic view.
Dynamic Mark to Edit Annotations
Sometimes just displaying a mark is not enough, need a way to edit it itself. For it in case, the library provides the useMark
hook.
The useMark
hook returns the label
and the value
of annotation as part of the state, the onChange
and the onRemove
handlers, also the reg
for register the ref of a mark to do it focusable by key operations.
In this example, let’s expand the Mark component do it is editable:
import {MarkedInput, useMark} from "rc-marked-input";
const Mark = () => {
const {label, onChange} = useMark()
const handleInput = (e) =>
onChange({label: e.currentTarget.textContent ?? "", value: " "}, {silent: true})
return <mark contentEditable onInput={handleInput} children={label}/>
}
export const Dynamic = () => {
const [value, setValue] = useState("Hello, dynamical mark @[world]( )!")
return <MarkedInput Mark={Mark} value={value} onChange={setValue}/>
}
The value is white space because of the default markup and unused.
The silent: true
option means no re-render itself by an annotation changed that can be useful for uncontrolled components.
The useMark
allow to avoid props passing and create a Dynamic Mark that can change its annotation and increase the level of interaction.
Custom Overlay to Add Annotations
We have the way to display and edit annotations. The last thing missing is the adding a new one. An Overlay that appears by a trigger is a good way to do it.
By default, a trigger is the @
character. After entering this or select position after its invoking an Overlay.
In this example, the header used like overlay:
import {MarkedInput} from "rc-marked-input";
const Overlay = () => <h1>I am the overlay</h1>
export const CustomOverlay = () => {
const [value, setValue] = useState("Hello, custom overlay by trigger @!")
return <MarkedInput Mark={Mark} Overlay={Overlay} value={value} onChange={setValue}/>
}
As props in overlay passed the onSelect
handler to add a new annotation instead of a triggered value, the trigger
object with triggered value string, and the style
object with the left
and the right
prop of the current caret position.
The overlay can be used for creating suggestions, pop-up, modal window, event listener, etc.
Data for these components can use the usual ways to get it, such as fetch, context, etc.
Configure for Multi Annotations
There is a need to configure the Marked Input in advanced cases as:
- Customize
markup
- Customize
trigger
- Pass
data
for an overlay (or for the defaultSuggestions
component) - Multi
Option
's for some type of mark and/or overlay - Adapting the passing props with the
initMark
and theinitOverlay
For example, let’s configure the Marked Input for two types of the Button as a mark component.
Markups:
const Primary = "@[__label__](primary:__value__)"
const Default = "@[__label__](default)"
Data for the Suggestions used to add a new annotation:
const Data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"]
const AnotherData = ["Seventh", "Eight", "Ninth"]
Then the Marked Input would look like this:
import {MarkedInput, Option} from "rc-marked-input";
export const App = () => {
const [value, setValue] = useState(
"Enter the '@' for creating @[Primary Mark](primary:Hello!) or '/' for @[Default mark](default)!"
)
return (
<MarkedInput Mark={Button} value={value} onChange={setValue}>
<Option
markup={Primary}
data={Data}
initMark={({label, value}) => ({label, primary: true, onClick: () => alert(value)})}
/>
<Option
markup={Default}
trigger="/"
data={AnotherData}
/>
</MarkedInput>
)
}
- The
Option
component used to configure the Marked Input. - The
initMark
for pass extra associative information like theprimary
flag or theonClick
handler. - In the second
Option
overridden thetrigger
prop because need to pass another data for suggestions.
The resulting structure doesn't look nice. To correct this injustice is the createMarkedInput
helper. It allows to encapsulate static props and to create the one row component input:
import {createMarkedInput} from "rc-marked-input";
const ConfiguredMarkedInput = createMarkedInput(Button, [{
markup: Primary,
data: Data,
initMark: ({label, value}) => ({label, primary: true, onClick: () => alert(value)})
}, {
trigger: '/',
markup: Default,
data: AnotherData
}])
const App = () => {
const [value, setValue] = useState(
"Enter the '@' for creating @[Primary Mark](primary:Hello!) or '/' for @[Default mark](default)!"
)
return <ConfiguredMarkedInput value={value} onChange={setValue}/>
}
In addition, the library provides also the annotate
for create annotations and the denote
for transform annotations to text helpers.
The sand box of configured Marked Input:
Operations with options appear to realize a conditional display or a logic that can help to create more complex components.
The provided helpers allow to close some cases, not relying only on the Marked Input.
Conclusion
The rc-marked-input library appears to use directly realized such components like a Mark and an Overlay. In combination with editable text, it becoming a kind of utility that helps easy to create impressive high-level components.
This library is young and stays in the active development. If you want to contribute, submit a PR, open an issue, or start a discussion!
Posted on November 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.