The Utility Component for an Annotated Text

nowely

Ruslan

Posted on November 18, 2022

The Utility Component for an Annotated Text

Example of the Configured Marked Input Component
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:

HTML Markup

As an annotation, it is not always handy to use the HTML markup for display:

Custom Markup

Then control over a display component allow to simplify markup and use exotic visual representation and inner logic:

Custom Display

Moreover, it allows to use a markup to forward information for conditional display:

Conditional Custom 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}/>
}
Enter fullscreen mode Exit fullscreen mode

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}/>
}
Enter fullscreen mode Exit fullscreen mode

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}/>
}
Enter fullscreen mode Exit fullscreen mode

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 default Suggestions component)
  • Multi Option's for some type of mark and/or overlay
  • Adapting the passing props with the initMark and the initOverlay

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)"
Enter fullscreen mode Exit fullscreen mode

Data for the Suggestions used to add a new annotation:

const Data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"]
const AnotherData = ["Seventh", "Eight", "Ninth"]
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode
  • The Option component used to configure the Marked Input.
  • The initMark for pass extra associative information like the primary flag or the onClick handler.
  • In the second Option overridden the trigger 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}/>
}
Enter fullscreen mode Exit fullscreen mode

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!

More examples in Storybook. Source code in GitHub.

💖 💪 🙅 🚩
nowely
Ruslan

Posted on November 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related